tutuca 0.9.55 → 0.9.57

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.
@@ -3326,14 +3326,18 @@ class ComponentSummary {
3326
3326
  }
3327
3327
 
3328
3328
  class ComponentList {
3329
- constructor({ items }) {
3329
+ constructor({ items, total = null, truncated = false }) {
3330
3330
  this.items = items;
3331
+ this.total = total ?? items.length;
3332
+ this.truncated = truncated;
3331
3333
  }
3332
3334
  }
3333
3335
 
3334
3336
  class ExampleIndex {
3335
- constructor({ sections }) {
3337
+ constructor({ sections, total = null, truncated = false }) {
3336
3338
  this.sections = sections;
3339
+ this.total = total ?? sections.reduce((n, s) => n + (s.items?.length ?? 0), 0);
3340
+ this.truncated = truncated;
3337
3341
  }
3338
3342
  }
3339
3343
 
@@ -3815,13 +3819,37 @@ function summarize(comp) {
3815
3819
  fields
3816
3820
  });
3817
3821
  }
3818
- function listComponents(normalized, { name = null } = {}) {
3822
+ function listComponents(normalized, { name = null, limit = 0 } = {}) {
3819
3823
  const comps = normalized.components;
3820
3824
  const picked = name === null ? comps : comps.filter((c) => c.name === name);
3821
- return new ComponentList({ items: picked.map(summarize) });
3825
+ const total = picked.length;
3826
+ const capped = limit > 0 ? picked.slice(0, limit) : picked;
3827
+ return new ComponentList({
3828
+ items: capped.map(summarize),
3829
+ total,
3830
+ truncated: capped.length < total
3831
+ });
3822
3832
  }
3823
- function listExamples(normalized) {
3824
- return new ExampleIndex({ sections: normalized.sections });
3833
+ function listExamples(normalized, { limit = 0 } = {}) {
3834
+ const sections = normalized.sections;
3835
+ const total = sections.reduce((n, s) => n + (s.items?.length ?? 0), 0);
3836
+ if (limit <= 0) {
3837
+ return new ExampleIndex({ sections, total, truncated: false });
3838
+ }
3839
+ const capped = [];
3840
+ let remaining = limit;
3841
+ for (const s of sections) {
3842
+ if (remaining <= 0)
3843
+ break;
3844
+ const items = s.items.slice(0, remaining);
3845
+ capped.push({ ...s, items });
3846
+ remaining -= items.length;
3847
+ }
3848
+ return new ExampleIndex({
3849
+ sections: capped,
3850
+ total,
3851
+ truncated: capped.reduce((n, s) => n + s.items.length, 0) < total
3852
+ });
3825
3853
  }
3826
3854
  var init_list = () => {};
3827
3855
 
@@ -8726,7 +8754,8 @@ var init_transactor = __esm(() => {
8726
8754
  }
8727
8755
  handlerProp = null;
8728
8756
  getHandlerForName(comp) {
8729
- return comp?.[this.handlerProp]?.[this.name] ?? nullHandler;
8757
+ const handlers = comp?.[this.handlerProp];
8758
+ return handlers?.[this.name] ?? handlers?.$unknown ?? nullHandler;
8730
8759
  }
8731
8760
  getHandlerAndArgs(_root, instance, comps) {
8732
8761
  const handler = this.getHandlerForName(comps.getCompFor(instance));
@@ -8754,6 +8783,9 @@ var init_transactor = __esm(() => {
8754
8783
  }
8755
8784
  };
8756
8785
  EventContext = class EventContext extends Dispatcher {
8786
+ get name() {
8787
+ return this.parent?.name ?? null;
8788
+ }
8757
8789
  stopPropagation() {
8758
8790
  return this.parent.stopPropagation();
8759
8791
  }
@@ -14082,6 +14114,12 @@ var exports__registry = {};
14082
14114
  __export(exports__registry, {
14083
14115
  COMMANDS: () => COMMANDS
14084
14116
  });
14117
+ function parseLimit(raw) {
14118
+ if (raw === undefined || raw === null)
14119
+ return 0;
14120
+ const n = Number.parseInt(raw, 10);
14121
+ return Number.isFinite(n) && n >= 0 ? n : 0;
14122
+ }
14085
14123
  var COMMANDS;
14086
14124
  var init__registry = __esm(() => {
14087
14125
  init_chai();
@@ -14092,7 +14130,7 @@ var init__registry = __esm(() => {
14092
14130
  init_render2();
14093
14131
  init_test();
14094
14132
  COMMANDS = {
14095
- info: {
14133
+ get: {
14096
14134
  describe: "Summarize the module's exports and counts.",
14097
14135
  defaultFormat: "cli",
14098
14136
  run: (normalized) => describeModule(normalized.mod, { path: normalized.path })
@@ -14100,15 +14138,20 @@ var init__registry = __esm(() => {
14100
14138
  list: {
14101
14139
  describe: "List components in the module.",
14102
14140
  defaultFormat: "cli",
14103
- run: (normalized) => listComponents(normalized)
14141
+ parseOptions: { limit: { type: "string" } },
14142
+ run: (normalized, { values, positionals }) => listComponents(normalized, {
14143
+ name: positionals[0] ?? null,
14144
+ limit: parseLimit(values.limit)
14145
+ })
14104
14146
  },
14105
14147
  examples: {
14106
14148
  describe: "List examples in the module.",
14107
14149
  defaultFormat: "cli",
14108
- run: (normalized) => listExamples(normalized)
14150
+ parseOptions: { limit: { type: "string" } },
14151
+ run: (normalized, { values }) => listExamples(normalized, { limit: parseLimit(values.limit) })
14109
14152
  },
14110
- docs: {
14111
- describe: "Produce API docs for components (optional <name> for one).",
14153
+ show: {
14154
+ describe: "Show API docs for components (optional <name> for one).",
14112
14155
  defaultFormat: "md",
14113
14156
  run: (normalized, { positionals }) => docComponents(normalized, { name: positionals[0] ?? null })
14114
14157
  },
@@ -14159,160 +14202,807 @@ var init__registry = __esm(() => {
14159
14202
  };
14160
14203
  });
14161
14204
 
14162
- // tools/cli/commands/install-skill.js
14163
- var exports_install_skill = {};
14164
- __export(exports_install_skill, {
14165
- run: () => run,
14166
- describe: () => describe
14167
- });
14168
- import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
14169
- import { homedir } from "node:os";
14170
- import { dirname, resolve } from "node:path";
14171
- import { fileURLToPath } from "node:url";
14172
- import { parseArgs } from "node:util";
14173
- function findSkillsRoot() {
14174
- const here = dirname(fileURLToPath(import.meta.url));
14175
- const candidates = [resolve(here, "..", "..", "..", "skill"), resolve(here, "..", "skill")];
14176
- for (const c of candidates) {
14177
- if (existsSync(resolve(c, "tutuca", "SKILL.md")))
14178
- return c;
14205
+ // tools/format/lint.js
14206
+ function unsupportedExprMessage(info) {
14207
+ const v = JSON.stringify(info.value);
14208
+ const label = UNSUPPORTED_EXPR_LABEL[info.detected] ?? "expression";
14209
+ switch (info.role) {
14210
+ case "attr":
14211
+ return `Unsupported ${label} ${v} in dynamic attribute ':${info.attr}'`;
14212
+ case "directive":
14213
+ return `Unsupported ${label} ${v} in directive '@${info.directive}'`;
14214
+ case "if":
14215
+ return `Unsupported ${label} ${v} in '@if.${info.attr}' condition`;
14216
+ case "x-op":
14217
+ return `Unsupported ${label} ${v} in <x ${info.op}>`;
14218
+ default:
14219
+ return `Unsupported ${label} ${v}`;
14179
14220
  }
14180
- return null;
14181
- }
14182
- function targetDir(scope, name, dotAgents) {
14183
- const base = scope === "user" ? homedir() : process.cwd();
14184
- const dir = dotAgents ? ".agents/skills" : ".claude/skills";
14185
- return resolve(base, dir, name);
14186
14221
  }
14187
- function targetHasSkillFiles(dir) {
14188
- if (!existsSync(dir))
14189
- return false;
14190
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
14191
- if (entry.isFile() && entry.name.endsWith(".md"))
14192
- return true;
14193
- if (entry.isDirectory() && targetHasSkillFiles(resolve(dir, entry.name)))
14194
- return true;
14222
+ function badValueMessage(info) {
14223
+ const v = JSON.stringify(info.value);
14224
+ switch (info.role) {
14225
+ case "attr":
14226
+ return `Cannot parse value ${v} for attribute ':${info.attr}'`;
14227
+ case "directive":
14228
+ return `Cannot parse value ${v} for directive '@${info.directive}'`;
14229
+ case "if":
14230
+ return `Cannot parse condition ${v} for '@if.${info.attr}'`;
14231
+ case "x-op":
14232
+ return `Cannot parse value ${v} for <x ${info.op}>`;
14233
+ case "handler-name":
14234
+ return `Cannot parse handler name ${v}`;
14235
+ case "handler-arg":
14236
+ return `Cannot parse handler argument ${v}`;
14237
+ case "macro-var":
14238
+ return `Macro variable '^${info.name}' is not defined`;
14239
+ default:
14240
+ return `Cannot parse value ${v}`;
14195
14241
  }
14196
- return false;
14197
14242
  }
14198
- function installSkill(skill, root, scope, force, dotAgents) {
14199
- const src = resolve(root, skill.srcSubdir);
14200
- if (!existsSync(resolve(src, "SKILL.md"))) {
14201
- process.stderr.write(`tutuca: missing skill assets for ${skill.name} at ${src}
14202
- `);
14203
- process.exit(1);
14204
- }
14205
- const target = targetDir(scope, skill.name, dotAgents);
14206
- if (targetHasSkillFiles(target) && !force) {
14207
- process.stderr.write(`tutuca: ${target} already contains skill files. Re-run with --force to overwrite.
14208
- `);
14209
- process.exit(1);
14210
- }
14211
- mkdirSync(target, { recursive: true });
14212
- cpSync(src, target, { recursive: true });
14213
- const baseDir = dotAgents ? ".agents/skills" : ".claude/skills";
14214
- const rel = scope === "project" ? `${baseDir}/${skill.name}` : target;
14215
- process.stdout.write(`installed ${skill.name} skill → ${rel}
14216
- `);
14243
+ function tagDisplay(tag) {
14244
+ return tag ? String(tag).toLowerCase() : null;
14217
14245
  }
14218
- async function run(argv) {
14219
- const parsed = parseArgs({
14220
- args: argv,
14221
- options: {
14222
- user: { type: "boolean", default: false },
14223
- project: { type: "boolean", default: false },
14224
- "margaui-skill": { type: "boolean", default: false },
14225
- "immutable-skill": { type: "boolean", default: false },
14226
- all: { type: "boolean", default: false },
14227
- "dot-agents": { type: "boolean", default: false },
14228
- force: { type: "boolean", short: "f", default: false },
14229
- help: { type: "boolean", short: "h", default: false }
14230
- },
14231
- allowPositionals: false
14232
- });
14233
- if (parsed.values.help) {
14234
- process.stdout.write(`tutuca install-skill [--user | --project] [--margaui-skill | --immutable-skill | --all] [--dot-agents] [--force]
14235
- ` + `
14236
- ` + ` Installs Claude Code skill assets into .claude/skills/<name>/.
14237
- ` + ` Defaults to --project (cwd); --user installs at ~/.claude/skills/.
14238
- ` + `
14239
- ` + ` Selection:
14240
- ` + ` (default) install the tutuca skill
14241
- ` + ` --margaui-skill install the margaui skill instead
14242
- ` + ` --immutable-skill install the immutable-js skill instead
14243
- ` + ` --all install every bundled skill (tutuca + margaui + immutable-js)
14244
- ` + `
14245
- ` + ` --dot-agents installs into .agents/skills/ instead of .claude/skills/.
14246
- ` + ` --force overwrites existing files.
14247
- `);
14248
- return;
14249
- }
14250
- if (parsed.values.user && parsed.values.project) {
14251
- process.stderr.write(`tutuca: --user and --project are mutually exclusive
14252
- `);
14253
- process.exit(1);
14254
- }
14255
- const selectionFlags = ["margaui-skill", "immutable-skill", "all"].filter((k) => parsed.values[k]);
14256
- if (selectionFlags.length > 1) {
14257
- process.stderr.write(`tutuca: ${selectionFlags.map((f) => `--${f}`).join(", ")} are mutually exclusive
14258
- `);
14259
- process.exit(1);
14260
- }
14261
- const scope = parsed.values.user ? "user" : "project";
14262
- const root = findSkillsRoot();
14263
- if (!root) {
14264
- process.stderr.write(`tutuca: skill assets not found alongside this CLI.
14265
- ` + "If you're running from a checkout, run `bun scripts/build-skill.js` first.\n");
14266
- process.exit(1);
14246
+ function fmtTagSuffix(info) {
14247
+ const t = tagDisplay(info?.tag);
14248
+ return t && t !== "x" ? ` on <${t}>` : "";
14249
+ }
14250
+ function fmtOriginSuffix(info) {
14251
+ if (!info)
14252
+ return "";
14253
+ const parts = [];
14254
+ if (info.originAttr) {
14255
+ const branch = info.branch ? `[${info.branch}]` : "";
14256
+ parts.push(`in ${info.originAttr}${branch}`);
14267
14257
  }
14268
- let selected;
14269
- if (parsed.values.all) {
14270
- selected = SKILLS;
14271
- } else {
14272
- const selFlag = SKILLS.find((s) => s.flag && parsed.values[s.flag]);
14273
- selected = selFlag ? [selFlag] : SKILLS.filter((s) => s.name === "tutuca");
14258
+ if (info.handlerName) {
14259
+ parts.push(`handler '${info.handlerName}'${info.argIndex !== undefined ? ` arg ${info.argIndex}` : ""}`);
14274
14260
  }
14275
- for (const skill of selected) {
14276
- installSkill(skill, root, scope, parsed.values.force, parsed.values["dot-agents"]);
14261
+ const t = tagDisplay(info.tag);
14262
+ if (t && t !== "x")
14263
+ parts.push(`on <${t}>`);
14264
+ return parts.length ? ` (${parts.join(", ")})` : "";
14265
+ }
14266
+ function fmtEventSuffix(info) {
14267
+ if (info?.originAttr)
14268
+ return ` in ${info.originAttr}`;
14269
+ if (info?.eventName)
14270
+ return ` in @on.${info.eventName}`;
14271
+ return "";
14272
+ }
14273
+ function lintIdToMessage(id, info) {
14274
+ switch (id) {
14275
+ case "RENDER_IT_OUTSIDE_OF_LOOP":
14276
+ return "<x render-it> used outside of a loop";
14277
+ case "UNKNOWN_EVENT_MODIFIER": {
14278
+ const mods = info.handler?.modifiers ?? [info.modifier];
14279
+ const written = `@on.${info.name}+${mods.join("+")}`;
14280
+ return `Unknown modifier '+${info.modifier}' in '${written}'`;
14281
+ }
14282
+ case "UNKNOWN_HANDLER_ARG_NAME":
14283
+ return `Unknown handler argument '${info.name}'${fmtOriginSuffix(info)}`;
14284
+ case "INPUT_HANDLER_NOT_IMPLEMENTED":
14285
+ return `Input handler '${info.name}' is not implemented${fmtEventSuffix(info)}`;
14286
+ case "INPUT_HANDLER_NOT_REFERENCED":
14287
+ return `Input handler '${info.name}' is defined but never used — remove it or wire it to an @on.* event`;
14288
+ case "INPUT_HANDLER_METHOD_NOT_IMPLEMENTED":
14289
+ return `Method '.${info.name}' is not implemented${fmtEventSuffix(info)}`;
14290
+ case "INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD":
14291
+ return `'.${info.name}' is a method reference, but '${info.name}' is defined as an input handler${fmtEventSuffix(info)}`;
14292
+ case "INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER":
14293
+ return `'${info.name}' is an input handler reference, but '${info.name}' is defined as a method${fmtEventSuffix(info)}`;
14294
+ case "FIELD_VAL_NOT_DEFINED":
14295
+ return `Field '.${info.name}' is not defined${fmtOriginSuffix(info)}`;
14296
+ case "DUPLICATE_ATTR_DEFINITION": {
14297
+ const sources = info.sources?.length ? ` (${info.sources.join(", ")})` : "";
14298
+ const tag = info.tag ? ` on <${String(info.tag).toLowerCase()}>` : "";
14299
+ return `Attribute '${info.name}' is set ${info.sources?.length ?? "multiple"} times${sources}${tag}`;
14300
+ }
14301
+ case "IF_NO_BRANCH_SET":
14302
+ return `'@if.${info.attr}' has no '@then' or '@else' branch — add '@then="…"' or '@else="…"' (or both)${fmtTagSuffix(info)}`;
14303
+ case "UNKNOWN_REQUEST_NAME":
14304
+ return `Unknown request '!${info.name}'${fmtOriginSuffix(info)}`;
14305
+ case "UNKNOWN_COMPONENT_NAME":
14306
+ return `Unknown component '${info.name}'${fmtOriginSuffix(info)}`;
14307
+ case "ALT_HANDLER_NOT_DEFINED":
14308
+ return `Alter handler '${info.name}' is not defined${fmtOriginSuffix(info)}`;
14309
+ case "ALT_HANDLER_NOT_REFERENCED":
14310
+ return `Alter handler '${info.name}' is defined but never used — remove it or reference it from @when, @enrich-with, or @loop-with`;
14311
+ case "UNKNOWN_MACRO_ARG":
14312
+ return `Argument '${info.name}' is not declared in macro '${info.macroName}'`;
14313
+ case "UNKNOWN_DIRECTIVE":
14314
+ return `Unknown directive '@${info.name}=${JSON.stringify(info.value)}'${fmtTagSuffix(info)}`;
14315
+ case "UNKNOWN_X_OP":
14316
+ return `Unknown <x> op '${info.name}=${JSON.stringify(info.value)}'${fmtTagSuffix(info)}`;
14317
+ case "UNKNOWN_X_ATTR":
14318
+ return `Unknown attribute '${info.name}=${JSON.stringify(info.value)}' on <x ${info.op}>${fmtTagSuffix(info)}`;
14319
+ case "MAYBE_DROP_AT_PREFIX": {
14320
+ const written = info.value !== undefined ? `${info.name}=${JSON.stringify(info.value)}` : info.name;
14321
+ return `'${written}' on <x> looks like a directive but is actually an x op/attr written with a leading '@'`;
14322
+ }
14323
+ case "BAD_VALUE":
14324
+ return `${badValueMessage(info)}${fmtTagSuffix(info)}`;
14325
+ case "UNSUPPORTED_EXPR_SYNTAX":
14326
+ return `${unsupportedExprMessage(info)}${fmtTagSuffix(info)}`;
14327
+ case "REDUNDANT_TEMPLATE_STRING":
14328
+ return `Redundant template string — '{${info.simpler}}' should be just '${info.simpler}'${fmtOriginSuffix(info)}`;
14329
+ case "UNKNOWN_COMPONENT_SPEC_KEY":
14330
+ return `Unknown component spec key '${info.key}' — value will be ignored at runtime`;
14331
+ case "HTML_TAG_NAME_HAS_UPPERCASE":
14332
+ return `Tag <${info.raw}> will be lowercased to <${info.lowercased}>${fmtLocationSuffix(info)}`;
14333
+ case "HTML_SVG_TAG_WILL_LOWERCASE":
14334
+ return `SVG tag <${info.raw}> is not in the WHATWG case-correction list — will be lowercased to <${info.lowercased}>${fmtLocationSuffix(info)}`;
14335
+ case "HTML_TAG_NOT_ALLOWED_IN_PARENT":
14336
+ return `<${info.tag}> not allowed under <${info.parent ?? "?"}> in ${info.mode} — ${htmlActionPhrase(info.action, info.tag, info.parent)}${fmtLocationSuffix(info)}`;
14337
+ case "HTML_TEXT_NOT_ALLOWED_IN_PARENT":
14338
+ return `Non-whitespace text not allowed in ${info.mode}: ${JSON.stringify(info.snippet)}${fmtLocationSuffix(info)}`;
14339
+ case "HTML_VOID_ELEMENT_HAS_CLOSE_TAG":
14340
+ return `Void element <${info.tag}> has an explicit close tag${fmtLocationSuffix(info)}`;
14341
+ case "HTML_DUPLICATE_FORM":
14342
+ return `Nested <form> — the inner form will be dropped by the parser${fmtLocationSuffix(info)}`;
14343
+ case "HTML_NESTED_INTERACTIVE":
14344
+ return `<${info.tag}> nested inside another <${info.tag}> — adoption agency will reorder${fmtLocationSuffix(info)}`;
14345
+ case "HTML_MISNESTED_FORMATTING":
14346
+ return `Misnested formatting tag </${info.tag}> — adoption agency will reorder nodes${fmtLocationSuffix(info)}`;
14347
+ case "HTML_UNEXPECTED_END_TAG":
14348
+ return `Unexpected end tag </${info.tag}>${fmtLocationSuffix(info)}`;
14349
+ case "HTML_UNCLOSED_BEFORE_END":
14350
+ return `<${info.unclosed}> still open when </${info.tag}> was seen — implicitly closed${fmtLocationSuffix(info)}`;
14351
+ case "HTML_DUPLICATE_ATTRIBUTE":
14352
+ return `Duplicate attribute '${info.name}' — second occurrence dropped${fmtLocationSuffix(info)}`;
14353
+ case "HTML_ATTRIBUTES_ON_END_TAG":
14354
+ return `Attributes on end tag </${info.tag}> — dropped by the parser${fmtLocationSuffix(info)}`;
14355
+ case "HTML_SELF_CLOSING_END_TAG":
14356
+ return `Self-closing end tag </${info.tag}/> — trailing '/' is meaningless${fmtLocationSuffix(info)}`;
14357
+ case "HTML_MISSING_ATTRIBUTE_VALUE":
14358
+ return `Attribute '${info.name}' is missing a value${fmtLocationSuffix(info)}`;
14359
+ case "HTML_CDATA_IN_HTML_NAMESPACE":
14360
+ return `CDATA section in HTML namespace — reinterpreted as a bogus comment${fmtLocationSuffix(info)}`;
14361
+ case "HTML_BOGUS_COMMENT":
14362
+ return `Bogus comment — content dropped by the parser${fmtLocationSuffix(info)}`;
14363
+ case "HTML_SVG_ATTR_WILL_LOWERCASE":
14364
+ return `SVG attribute '${info.raw}' will be rewritten to '${info.canonical}'${fmtLocationSuffix(info)}`;
14365
+ case "HTML_MATHML_ATTR_WILL_LOWERCASE":
14366
+ return `MathML attribute '${info.raw}' will be rewritten to '${info.canonical}'${fmtLocationSuffix(info)}`;
14367
+ case "LINT_ERROR":
14368
+ return info.message;
14369
+ default:
14370
+ return id;
14277
14371
  }
14278
- process.stdout.write(`Open a Claude Code session in this directory to use it.
14279
- `);
14280
14372
  }
14281
- var describe = "Install Claude Code skills (tutuca, margaui, immutable-js) into .claude/skills/.", SKILLS;
14282
- var init_install_skill = __esm(() => {
14283
- SKILLS = [
14373
+ function suggestionToMessage(suggestion) {
14374
+ if (!suggestion)
14375
+ return null;
14376
+ switch (suggestion.kind) {
14377
+ case "replace-name":
14378
+ return `did you mean '${suggestion.to}'?`;
14379
+ case "drop-prefix":
14380
+ return `did you mean '${suggestion.to}'? (drop the leading '${suggestion.from.slice(0, suggestion.from.length - suggestion.to.length)}')`;
14381
+ case "add-prefix":
14382
+ return `did you mean '${suggestion.to}'? (add the leading '${suggestion.to.slice(0, suggestion.to.length - suggestion.from.length)}')`;
14383
+ case "remove":
14384
+ return `remove ${suggestion.what}`;
14385
+ case "rewrite":
14386
+ return `use '${suggestion.to}' instead of '${suggestion.from}'`;
14387
+ case "wrap":
14388
+ return `wrap it in ${suggestion.to}`;
14389
+ case "rephrase":
14390
+ return suggestion.text ?? null;
14391
+ default:
14392
+ return null;
14393
+ }
14394
+ }
14395
+ function fmtLocationSuffix(info) {
14396
+ const loc = info?.location;
14397
+ if (!loc)
14398
+ return "";
14399
+ return ` at line ${loc.line}, col ${loc.column}`;
14400
+ }
14401
+ function htmlActionPhrase(action, tag, parent) {
14402
+ switch (action) {
14403
+ case "ignored":
14404
+ return `the parser will drop this <${tag}>`;
14405
+ case "drop":
14406
+ return `the parser will drop this <${tag}>`;
14407
+ case "auto-close-implicit":
14408
+ return `the parser will close <${parent ?? "?"}> first, then place <${tag}> as a sibling`;
14409
+ case "foster-parent":
14410
+ return `the parser will move <${tag}> outside <${parent ?? "?"}> (foster-parenting)`;
14411
+ case "foreign-breakout":
14412
+ return `the parser will exit foreign content and re-process <${tag}> in HTML mode`;
14413
+ default:
14414
+ return `parser action: ${action}`;
14415
+ }
14416
+ }
14417
+ var UNSUPPORTED_EXPR_LABEL;
14418
+ var init_lint2 = __esm(() => {
14419
+ UNSUPPORTED_EXPR_LABEL = {
14420
+ ternary: "ternary expression",
14421
+ comparison: "comparison",
14422
+ logical: "logical expression",
14423
+ "call-with-args": "method call with arguments"
14424
+ };
14425
+ });
14426
+
14427
+ // tools/cli/errors.js
14428
+ var exports_errors = {};
14429
+ __export(exports_errors, {
14430
+ parseArgsErrorShape: () => parseArgsErrorShape,
14431
+ emitError: () => emitError,
14432
+ didYouMean: () => didYouMean,
14433
+ CODES: () => CODES
14434
+ });
14435
+ function isJsonFormat(opts) {
14436
+ return opts?.format === "json";
14437
+ }
14438
+ function emitError(opts, shape) {
14439
+ const exit = shape.exit ?? 1;
14440
+ if (isJsonFormat(opts)) {
14441
+ const envelope = {
14442
+ error: {
14443
+ code: shape.code ?? CODES.INTERNAL,
14444
+ message: shape.message
14445
+ }
14446
+ };
14447
+ if (shape.suggestion)
14448
+ envelope.error.suggestion = shape.suggestion;
14449
+ if (shape.hint)
14450
+ envelope.error.hint = shape.hint;
14451
+ if (shape.where)
14452
+ envelope.error.where = shape.where;
14453
+ process.stderr.write(`${JSON.stringify(envelope)}
14454
+ `);
14455
+ } else {
14456
+ const where = shape.where ? `[${shape.where}] ` : "";
14457
+ process.stderr.write(`tutuca: ${where}${shape.message}
14458
+ `);
14459
+ const tail = suggestionToMessage(shape.suggestion);
14460
+ if (tail)
14461
+ process.stderr.write(` ${tail}
14462
+ `);
14463
+ if (shape.hint)
14464
+ process.stderr.write(` hint: ${shape.hint}
14465
+ `);
14466
+ }
14467
+ process.exit(exit);
14468
+ }
14469
+ function didYouMean(name, candidates) {
14470
+ const close = closestName(name, candidates);
14471
+ return close ? { kind: "replace-name", from: name, to: close } : null;
14472
+ }
14473
+ function parseArgsErrorShape(err, validFlags) {
14474
+ if (!err?.code?.startsWith?.("ERR_PARSE_ARGS_"))
14475
+ return null;
14476
+ if (err.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
14477
+ const m = err.message.match(/Unknown option '(-{1,2}[^']+)'/);
14478
+ const raw = m?.[1] ?? null;
14479
+ const stripped = raw?.replace(/^-+/, "");
14480
+ const candidates = [...validFlags ?? []];
14481
+ const close = stripped ? closestName(stripped, candidates) : null;
14482
+ return {
14483
+ code: CODES.USAGE_UNKNOWN_FLAG,
14484
+ message: raw ? `Unknown flag '${raw}'` : err.message,
14485
+ suggestion: close ? { kind: "replace-name", from: raw, to: `--${close}` } : null,
14486
+ hint: candidates.length ? `Valid flags: ${candidates.map((f) => `--${f}`).join(", ")}` : null
14487
+ };
14488
+ }
14489
+ if (err.code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE") {
14490
+ return { code: CODES.USAGE_BAD_FLAG_VALUE, message: err.message };
14491
+ }
14492
+ return { code: err.code, message: err.message };
14493
+ }
14494
+ var CODES;
14495
+ var init_errors = __esm(() => {
14496
+ init_lint2();
14497
+ CODES = {
14498
+ USAGE_UNKNOWN_COMMAND: "ERR_USAGE_UNKNOWN_COMMAND",
14499
+ USAGE_UNKNOWN_FLAG: "ERR_USAGE_UNKNOWN_FLAG",
14500
+ USAGE_BAD_FLAG_VALUE: "ERR_USAGE_BAD_FLAG_VALUE",
14501
+ USAGE_MISSING_MODULE: "ERR_USAGE_MISSING_MODULE",
14502
+ USAGE_MUTUALLY_EXCLUSIVE: "ERR_USAGE_MUTUALLY_EXCLUSIVE",
14503
+ USAGE_MISSING_ARGUMENT: "ERR_USAGE_MISSING_ARGUMENT",
14504
+ FORMAT_UNKNOWN: "ERR_FORMAT_UNKNOWN",
14505
+ FORMAT_UNSUPPORTED: "ERR_FORMAT_UNSUPPORTED",
14506
+ MODULE_LOAD_FAILED: "ERR_MODULE_LOAD_FAILED",
14507
+ MODULE_SHAPE_MISMATCH: "EXAMPLES_SHAPE_MISMATCH",
14508
+ SKILL_ASSETS_MISSING: "ERR_SKILL_ASSETS_MISSING",
14509
+ SKILL_TARGET_EXISTS: "ERR_SKILL_TARGET_EXISTS",
14510
+ INTERNAL: "ERR_INTERNAL"
14511
+ };
14512
+ });
14513
+
14514
+ // tools/cli/commands/feedback.js
14515
+ var exports_feedback = {};
14516
+ __export(exports_feedback, {
14517
+ run: () => run2,
14518
+ describe: () => describe2
14519
+ });
14520
+ import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
14521
+ import { homedir } from "node:os";
14522
+ import { dirname as dirname2, resolve as resolve2 } from "node:path";
14523
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
14524
+ import { parseArgs } from "node:util";
14525
+ function readPackageVersion() {
14526
+ const here = dirname2(fileURLToPath2(import.meta.url));
14527
+ const candidates = [
14528
+ resolve2(here, "..", "..", "..", "package.json"),
14529
+ resolve2(here, "..", "package.json")
14530
+ ];
14531
+ for (const p of candidates) {
14532
+ if (existsSync2(p)) {
14533
+ try {
14534
+ return JSON.parse(readFileSync2(p, "utf8")).version ?? null;
14535
+ } catch {}
14536
+ }
14537
+ }
14538
+ return null;
14539
+ }
14540
+ function readStdinSync() {
14541
+ if (process.stdin.isTTY)
14542
+ return "";
14543
+ try {
14544
+ return readFileSync2(0, "utf8");
14545
+ } catch {
14546
+ return "";
14547
+ }
14548
+ }
14549
+ async function run2(argv, opts = {}) {
14550
+ const parsed = parseArgs({
14551
+ args: argv,
14552
+ options: { help: { type: "boolean", short: "h", default: false } },
14553
+ allowPositionals: true
14554
+ });
14555
+ if (parsed.values.help) {
14556
+ process.stdout.write(HELP);
14557
+ return;
14558
+ }
14559
+ const positional = parsed.positionals.join(" ").trim();
14560
+ const message = positional || readStdinSync().trim();
14561
+ if (!message) {
14562
+ emitError(opts, {
14563
+ code: CODES.USAGE_MISSING_ARGUMENT,
14564
+ message: "feedback requires a message (positional arg or piped stdin)",
14565
+ hint: 'Example: `tutuca feedback "the lint code FIELD_VAL_NOT_DEFINED was confusing"` ' + "or `echo ... | tutuca feedback`."
14566
+ });
14567
+ }
14568
+ const record = {
14569
+ ts: new Date().toISOString(),
14570
+ version: readPackageVersion(),
14571
+ message
14572
+ };
14573
+ const dir = resolve2(homedir(), ".tutuca");
14574
+ const file = resolve2(dir, "feedback.jsonl");
14575
+ mkdirSync(dir, { recursive: true });
14576
+ appendFileSync(file, `${JSON.stringify(record)}
14577
+ `);
14578
+ process.stdout.write(`recorded feedback → ${file}
14579
+ `);
14580
+ }
14581
+ var describe2 = "Record freeform feedback (bug, confusion, suggestion) to ~/.tutuca/feedback.jsonl.", HELP;
14582
+ var init_feedback = __esm(() => {
14583
+ init_errors();
14584
+ HELP = `tutuca feedback [message]
14585
+ ` + `
14586
+ ` + ` Append a feedback record to ~/.tutuca/feedback.jsonl (created if missing).
14587
+ ` + ` Use this for bugs, confusing messages, or suggestions about the CLI,
14588
+ ` + ` bundled skills, docs, or the library itself.
14589
+ ` + `
14590
+ ` + ` Provide the message as a positional argument:
14591
+ ` + ` tutuca feedback "the lint code FIELD_VAL_NOT_DEFINED was confusing"
14592
+ ` + `
14593
+ ` + ` ...or via stdin (useful for multi-line notes):
14594
+ ` + ` echo "..." | tutuca feedback
14595
+ ` + ` tutuca feedback < notes.txt
14596
+ ` + `
14597
+ ` + ` Each record is one JSON object per line: {ts, version, message}.
14598
+ `;
14599
+ });
14600
+
14601
+ // tools/cli/commands/install-skill.js
14602
+ var exports_install_skill = {};
14603
+ __export(exports_install_skill, {
14604
+ run: () => run3,
14605
+ describe: () => describe3
14606
+ });
14607
+ import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync } from "node:fs";
14608
+ import { homedir as homedir2 } from "node:os";
14609
+ import { dirname as dirname3, relative, resolve as resolve3 } from "node:path";
14610
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
14611
+ import { parseArgs as parseArgs2 } from "node:util";
14612
+ function findSkillsRoot() {
14613
+ const here = dirname3(fileURLToPath3(import.meta.url));
14614
+ const candidates = [resolve3(here, "..", "..", "..", "skill"), resolve3(here, "..", "skill")];
14615
+ for (const c of candidates) {
14616
+ if (existsSync3(resolve3(c, "tutuca", "SKILL.md")))
14617
+ return c;
14618
+ }
14619
+ return null;
14620
+ }
14621
+ function targetDir(scope, name, dotAgents) {
14622
+ const base = scope === "user" ? homedir2() : process.cwd();
14623
+ const dir = dotAgents ? ".agents/skills" : ".claude/skills";
14624
+ return resolve3(base, dir, name);
14625
+ }
14626
+ function targetHasSkillFiles(dir) {
14627
+ if (!existsSync3(dir))
14628
+ return false;
14629
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
14630
+ if (entry.isFile() && entry.name.endsWith(".md"))
14631
+ return true;
14632
+ if (entry.isDirectory() && targetHasSkillFiles(resolve3(dir, entry.name)))
14633
+ return true;
14634
+ }
14635
+ return false;
14636
+ }
14637
+ function listFiles(srcRoot, srcDir, acc) {
14638
+ for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
14639
+ const full = resolve3(srcDir, entry.name);
14640
+ if (entry.isDirectory())
14641
+ listFiles(srcRoot, full, acc);
14642
+ else if (entry.isFile())
14643
+ acc.push(relative(srcRoot, full));
14644
+ }
14645
+ return acc;
14646
+ }
14647
+ function installSkill(skill, root, scope, force, dotAgents, dryRun, opts) {
14648
+ const src = resolve3(root, skill.srcSubdir);
14649
+ if (!existsSync3(resolve3(src, "SKILL.md"))) {
14650
+ emitError(opts, {
14651
+ code: CODES.SKILL_ASSETS_MISSING,
14652
+ message: `missing skill assets for '${skill.name}' at ${src}`,
14653
+ hint: "If you're running from a checkout, run `bun scripts/build-skill.js` first."
14654
+ });
14655
+ }
14656
+ const target = targetDir(scope, skill.name, dotAgents);
14657
+ if (targetHasSkillFiles(target) && !force) {
14658
+ emitError(opts, {
14659
+ code: CODES.SKILL_TARGET_EXISTS,
14660
+ message: `${target} already contains skill files`,
14661
+ hint: "Re-run with --force to overwrite, or --dry-run to see what would change."
14662
+ });
14663
+ }
14664
+ const baseDir = dotAgents ? ".agents/skills" : ".claude/skills";
14665
+ const rel = scope === "project" ? `${baseDir}/${skill.name}` : target;
14666
+ if (dryRun) {
14667
+ const files = listFiles(src, src, []);
14668
+ process.stdout.write(`would install ${skill.name} skill → ${rel}
14669
+ `);
14670
+ for (const f of files)
14671
+ process.stdout.write(` + ${f}
14672
+ `);
14673
+ return;
14674
+ }
14675
+ mkdirSync2(target, { recursive: true });
14676
+ cpSync(src, target, { recursive: true });
14677
+ process.stdout.write(`installed ${skill.name} skill → ${rel}
14678
+ `);
14679
+ }
14680
+ async function run3(argv, opts = {}) {
14681
+ const parsed = parseArgs2({
14682
+ args: argv,
14683
+ options: {
14684
+ user: { type: "boolean", default: false },
14685
+ project: { type: "boolean", default: false },
14686
+ "margaui-skill": { type: "boolean", default: false },
14687
+ "immutable-skill": { type: "boolean", default: false },
14688
+ all: { type: "boolean", default: false },
14689
+ "dot-agents": { type: "boolean", default: false },
14690
+ "dry-run": { type: "boolean", default: false },
14691
+ force: { type: "boolean", short: "f", default: false },
14692
+ help: { type: "boolean", short: "h", default: false }
14693
+ },
14694
+ allowPositionals: false
14695
+ });
14696
+ if (parsed.values.help) {
14697
+ process.stdout.write(`tutuca install-skill [--user | --project] [--margaui-skill | --immutable-skill | --all]
14698
+ ` + ` [--dot-agents] [--dry-run] [--force]
14699
+ ` + `
14700
+ ` + ` Installs Claude Code skill assets into .claude/skills/<name>/.
14701
+ ` + ` Defaults to --project (cwd); --user installs at ~/.claude/skills/.
14702
+ ` + `
14703
+ ` + ` Selection:
14704
+ ` + ` (default) install the tutuca skill
14705
+ ` + ` --margaui-skill install the margaui skill instead
14706
+ ` + ` --immutable-skill install the immutable-js skill instead
14707
+ ` + ` --all install every bundled skill (tutuca + margaui + immutable-js)
14708
+ ` + `
14709
+ ` + ` --dot-agents installs into .agents/skills/ instead of .claude/skills/.
14710
+ ` + ` --dry-run prints the files that would be written without touching the filesystem.
14711
+ ` + ` --force overwrites existing files.
14712
+ `);
14713
+ return;
14714
+ }
14715
+ if (parsed.values.user && parsed.values.project) {
14716
+ emitError(opts, {
14717
+ code: CODES.USAGE_MUTUALLY_EXCLUSIVE,
14718
+ message: "--user and --project are mutually exclusive",
14719
+ hint: "Use --user for ~/.claude/skills/ or --project (the default) for ./.claude/skills/."
14720
+ });
14721
+ }
14722
+ const selectionFlags = ["margaui-skill", "immutable-skill", "all"].filter((k) => parsed.values[k]);
14723
+ if (selectionFlags.length > 1) {
14724
+ emitError(opts, {
14725
+ code: CODES.USAGE_MUTUALLY_EXCLUSIVE,
14726
+ message: `${selectionFlags.map((f) => `--${f}`).join(", ")} are mutually exclusive`,
14727
+ hint: "Pick one skill, or use --all to install every bundled skill."
14728
+ });
14729
+ }
14730
+ const scope = parsed.values.user ? "user" : "project";
14731
+ const root = findSkillsRoot();
14732
+ if (!root) {
14733
+ emitError(opts, {
14734
+ code: CODES.SKILL_ASSETS_MISSING,
14735
+ message: "skill assets not found alongside this CLI",
14736
+ hint: "If you're running from a checkout, run `bun scripts/build-skill.js` first."
14737
+ });
14738
+ }
14739
+ let selected;
14740
+ if (parsed.values.all) {
14741
+ selected = SKILLS;
14742
+ } else {
14743
+ const selFlag = SKILLS.find((s) => s.flag && parsed.values[s.flag]);
14744
+ selected = selFlag ? [selFlag] : SKILLS.filter((s) => s.name === "tutuca");
14745
+ }
14746
+ for (const skill of selected) {
14747
+ installSkill(skill, root, scope, parsed.values.force, parsed.values["dot-agents"], parsed.values["dry-run"], opts);
14748
+ }
14749
+ if (!parsed.values["dry-run"]) {
14750
+ process.stdout.write(`Open a Claude Code session in this directory to use it.
14751
+ `);
14752
+ }
14753
+ }
14754
+ var describe3 = "Install Claude Code skills (tutuca, margaui, immutable-js) into .claude/skills/.", SKILLS;
14755
+ var init_install_skill = __esm(() => {
14756
+ init_errors();
14757
+ SKILLS = [
14284
14758
  { name: "tutuca", srcSubdir: "tutuca", flag: null },
14285
14759
  { name: "margaui", srcSubdir: "margaui", flag: "margaui-skill" },
14286
14760
  { name: "immutable-js", srcSubdir: "immutable-js", flag: "immutable-skill" }
14287
14761
  ];
14288
14762
  });
14289
14763
 
14764
+ // tools/cli/commands/agent-context.js
14765
+ init__registry();
14766
+ var exports_agent_context = {};
14767
+ __export(exports_agent_context, {
14768
+ run: () => run,
14769
+ describe: () => describe
14770
+ });
14771
+ import { existsSync, readFileSync } from "node:fs";
14772
+ import { dirname, resolve } from "node:path";
14773
+ import { fileURLToPath } from "node:url";
14774
+ var describe = "Print a machine-readable schema (commands, flags, exit codes) as JSON.";
14775
+ var SCHEMA_VERSION = 1;
14776
+ var GLOBAL_FLAGS = [
14777
+ {
14778
+ name: "json",
14779
+ type: "boolean",
14780
+ description: "Shorthand for --format=json. Errors also emit JSON on stderr."
14781
+ },
14782
+ {
14783
+ name: "format",
14784
+ short: "f",
14785
+ type: "string",
14786
+ enum: ["cli", "md", "json", "html"],
14787
+ description: "Output format. Per-command default below; html is render-only; json works for every module command."
14788
+ },
14789
+ {
14790
+ name: "output",
14791
+ short: "o",
14792
+ type: "string",
14793
+ description: "Write to file instead of stdout."
14794
+ },
14795
+ { name: "pretty", type: "boolean", description: "Pretty-print HTML/JSON output." },
14796
+ {
14797
+ name: "module",
14798
+ type: "string",
14799
+ description: "Alternative to first-positional module path."
14800
+ },
14801
+ { name: "help", short: "h", type: "boolean", description: "Show help." }
14802
+ ];
14803
+ var EXIT_CODES = [
14804
+ { code: 0, meaning: "success" },
14805
+ { code: 1, meaning: "usage error (bad args, missing module, bad module shape)" },
14806
+ { code: 2, meaning: "lint findings at error level (lint command)" },
14807
+ { code: 3, meaning: "render crash (render command)" },
14808
+ { code: 4, meaning: "test failures (test command)" }
14809
+ ];
14810
+ var ERROR_CODES = [
14811
+ "ERR_USAGE_UNKNOWN_COMMAND",
14812
+ "ERR_USAGE_UNKNOWN_FLAG",
14813
+ "ERR_USAGE_BAD_FLAG_VALUE",
14814
+ "ERR_USAGE_MISSING_MODULE",
14815
+ "ERR_USAGE_MISSING_ARGUMENT",
14816
+ "ERR_USAGE_MUTUALLY_EXCLUSIVE",
14817
+ "ERR_FORMAT_UNKNOWN",
14818
+ "ERR_FORMAT_UNSUPPORTED",
14819
+ "EXAMPLES_SHAPE_MISMATCH",
14820
+ "ERR_SKILL_ASSETS_MISSING",
14821
+ "ERR_SKILL_TARGET_EXISTS"
14822
+ ];
14823
+ var COMMAND_FLAGS = {
14824
+ list: [
14825
+ {
14826
+ name: "limit",
14827
+ type: "string",
14828
+ description: "Cap number of components emitted (0 = all). Truncated output is signaled by `truncated: true` in JSON."
14829
+ }
14830
+ ],
14831
+ examples: [
14832
+ {
14833
+ name: "limit",
14834
+ type: "string",
14835
+ description: "Cap total number of example items emitted (0 = all)."
14836
+ }
14837
+ ],
14838
+ render: [
14839
+ { name: "title", type: "string", description: "Filter by example title." },
14840
+ { name: "view", type: "string", description: "Override example's view name." }
14841
+ ],
14842
+ test: [
14843
+ {
14844
+ name: "grep",
14845
+ type: "string",
14846
+ description: "Substring match against the full test path."
14847
+ },
14848
+ {
14849
+ name: "bail",
14850
+ type: "boolean",
14851
+ description: "Stop on first failure; remaining tests reported as skip."
14852
+ }
14853
+ ]
14854
+ };
14855
+ var NO_MODULE_COMMANDS_META = {
14856
+ help: {
14857
+ describe: "Show usage. `help <command>` for per-command detail.",
14858
+ needsModule: false,
14859
+ flags: [{ name: "help", short: "h", type: "boolean" }],
14860
+ positionals: [{ name: "command", required: false }]
14861
+ },
14862
+ feedback: {
14863
+ describe: "Append a feedback record to ~/.tutuca/feedback.jsonl.",
14864
+ needsModule: false,
14865
+ flags: [{ name: "help", short: "h", type: "boolean" }],
14866
+ positionals: [
14867
+ {
14868
+ name: "message",
14869
+ required: false,
14870
+ note: "Falls back to piped stdin when omitted."
14871
+ }
14872
+ ]
14873
+ },
14874
+ "install-skill": {
14875
+ describe: "Copy bundled Claude Code skill assets into .claude/skills/<name>/.",
14876
+ needsModule: false,
14877
+ flags: [
14878
+ { name: "user", type: "boolean", description: "Install at ~/.claude/skills/." },
14879
+ {
14880
+ name: "project",
14881
+ type: "boolean",
14882
+ description: "Install at ./.claude/skills/ (default)."
14883
+ },
14884
+ { name: "margaui-skill", type: "boolean" },
14885
+ { name: "immutable-skill", type: "boolean" },
14886
+ { name: "all", type: "boolean", description: "Install every bundled skill." },
14887
+ {
14888
+ name: "dot-agents",
14889
+ type: "boolean",
14890
+ description: "Use .agents/skills/ instead of .claude/skills/."
14891
+ },
14892
+ {
14893
+ name: "dry-run",
14894
+ type: "boolean",
14895
+ description: "Print files that would be written; don't touch disk."
14896
+ },
14897
+ {
14898
+ name: "force",
14899
+ short: "f",
14900
+ type: "boolean",
14901
+ description: "Overwrite existing files."
14902
+ },
14903
+ { name: "help", short: "h", type: "boolean" }
14904
+ ],
14905
+ positionals: []
14906
+ },
14907
+ "agent-context": {
14908
+ describe: "Print this schema as JSON.",
14909
+ needsModule: false,
14910
+ flags: [],
14911
+ positionals: []
14912
+ }
14913
+ };
14914
+ function packageVersion() {
14915
+ const here = dirname(fileURLToPath(import.meta.url));
14916
+ const candidates = [
14917
+ resolve(here, "..", "..", "..", "package.json"),
14918
+ resolve(here, "..", "..", "package.json")
14919
+ ];
14920
+ for (const p of candidates) {
14921
+ if (existsSync(p)) {
14922
+ try {
14923
+ return JSON.parse(readFileSync(p, "utf8")).version ?? null;
14924
+ } catch {}
14925
+ }
14926
+ }
14927
+ return null;
14928
+ }
14929
+ function moduleCommandSchema(name, cmd) {
14930
+ return {
14931
+ name,
14932
+ describe: cmd.describe,
14933
+ needsModule: true,
14934
+ needsEnv: !!cmd.needsEnv,
14935
+ defaultFormat: cmd.defaultFormat,
14936
+ flags: COMMAND_FLAGS[name] ?? [],
14937
+ positionals: [
14938
+ {
14939
+ name: "module-path",
14940
+ required: true,
14941
+ description: "Path to the ES module to inspect."
14942
+ },
14943
+ {
14944
+ name: "name",
14945
+ required: false,
14946
+ description: "Component name filter (operate on one component)."
14947
+ }
14948
+ ]
14949
+ };
14950
+ }
14951
+ async function run() {
14952
+ const moduleCmds = Object.entries(COMMANDS).map(([name, cmd]) => moduleCommandSchema(name, cmd));
14953
+ const noModuleCmds = Object.entries(NO_MODULE_COMMANDS_META).map(([name, meta]) => ({
14954
+ name,
14955
+ ...meta
14956
+ }));
14957
+ const schema = {
14958
+ schemaVersion: SCHEMA_VERSION,
14959
+ cli: "tutuca",
14960
+ version: packageVersion(),
14961
+ invocation: {
14962
+ synopsis: "tutuca <command> <module-path> [name] [flags]",
14963
+ moduleFirst: false,
14964
+ moduleFlag: "module",
14965
+ note: "For module-required commands, the module path is the second positional. Use --module=<path> as an alternative."
14966
+ },
14967
+ globalFlags: GLOBAL_FLAGS,
14968
+ formats: ["cli", "md", "json", "html"],
14969
+ exitCodes: EXIT_CODES,
14970
+ errorCodes: ERROR_CODES,
14971
+ commands: [...moduleCmds, ...noModuleCmds]
14972
+ };
14973
+ process.stdout.write(`${JSON.stringify(schema, null, 2)}
14974
+ `);
14975
+ }
14976
+
14290
14977
  // tools/tutuca.js
14291
14978
  init__registry();
14979
+ init_feedback();
14292
14980
 
14293
14981
  // tools/cli/commands/help.js
14294
14982
  var exports_help = {};
14295
14983
  __export(exports_help, {
14296
- run: () => run2,
14297
- describe: () => describe2
14984
+ run: () => run4,
14985
+ describe: () => describe4
14298
14986
  });
14299
- var describe2 = "Show usage. `help <command>` for per-command detail.";
14987
+ var describe4 = "Show usage. `help <command>` for per-command detail.";
14300
14988
  var OVERVIEW = `tutuca — CLI for inspecting, documenting, linting and rendering tutuca
14301
14989
  components defined in an ES module.
14302
14990
 
14303
14991
  SYNOPSIS
14304
- tutuca <module-path> <command> [name] [flags]
14992
+ tutuca <command> <module-path> [name] [flags]
14305
14993
  tutuca help [command]
14306
14994
  tutuca {-h | --help}
14307
14995
 
14308
14996
  INVOCATION SHAPE
14309
- - <module-path> comes FIRST (before the command). It is a path to an ES
14310
- module resolvable by Node (absolute, or relative to cwd).
14997
+ - <command> comes FIRST. <module-path> is the second positional a path
14998
+ to an ES module resolvable by Node (absolute, or relative to cwd).
14999
+ Use --module=<path> as an alternative if the path conflicts.
14311
15000
  - [name] is an OPTIONAL component-name filter. Omit it to operate on all
14312
- components; pass it to operate on exactly one (e.g. \`docs Button\`).
14313
- - Flags may appear anywhere. Global flags and command flags share the
14314
- same argv; unknown flags are rejected by the subcommand's parser.
14315
- - \`help\` does NOT take a module path.
15001
+ components; pass it to operate on exactly one (e.g. \`show Button\`).
15002
+ - Per-command flags follow the module path; global flags can appear
15003
+ anywhere. Unknown flags are rejected by the subcommand's parser.
15004
+ - \`help\`, \`feedback\`, \`install-skill\`, \`agent-context\` do NOT take a
15005
+ module path.
14316
15006
 
14317
15007
  MODULE CONVENTION
14318
15008
  A module passed to tutuca must export one or more of:
@@ -14346,27 +15036,31 @@ MODULE CONVENTION
14346
15036
  export function getRoot() // optional; returned by info
14347
15037
 
14348
15038
  COMMANDS (require <module-path>)
14349
- info
15039
+ get <module>
14350
15040
  Summarize which getX() exports are present and count components,
14351
15041
  macros, request handlers, examples, and sections. Good first step.
14352
15042
 
14353
- list
15043
+ list <module> [name] [--limit <n>]
14354
15044
  List each component with its declared views and fields (name, type).
15045
+ --limit caps the number of components emitted (0 = all). Truncated
15046
+ output ends with a "… N more" footer in cli/md, or \`truncated:true\`
15047
+ in JSON.
14355
15048
 
14356
- examples
15049
+ examples <module> [--limit <n>]
14357
15050
  Print the module's example sections: title, description, items.
14358
15051
  Each item shows its resolved component name and view.
15052
+ --limit caps the total number of items emitted (0 = all).
14359
15053
 
14360
- docs [name]
14361
- Generate API docs (methods, input handlers, fields with their
15054
+ show <module> [name]
15055
+ Show API docs (methods, input handlers, fields with their
14362
15056
  auto-generated accessor/mutator methods) for every component, or
14363
15057
  for the single component whose name matches [name].
14364
15058
 
14365
- lint [name]
15059
+ lint <module> [name]
14366
15060
  Run the built-in component linter. Reports at levels error / warn
14367
15061
  / hint. Exits 2 if ANY finding is at error level.
14368
15062
 
14369
- render [name] [--title <t>] [--view <v>]
15063
+ render <module> [name] [--title <t>] [--view <v>]
14370
15064
  Render examples to HTML by running each example value through the
14371
15065
  component tree in a headless DOM. Filters:
14372
15066
  [name] only examples whose value is an instance of <name>
@@ -14374,7 +15068,7 @@ COMMANDS (require <module-path>)
14374
15068
  --view <v> override the example's view name
14375
15069
  Exits 3 if any render crashes.
14376
15070
 
14377
- test [name] [--grep <pattern>] [--bail]
15071
+ test <module> [name] [--grep <pattern>] [--bail]
14378
15072
  Run tests defined by getTests({ describe, test, expect }). Filters:
14379
15073
  [name] only tests whose tagged componentName equals <name>
14380
15074
  --grep <p> substring match against the full test path
@@ -14387,7 +15081,18 @@ COMMANDS (no module required)
14387
15081
  Without [command]: prints this full reference.
14388
15082
  With [command]: prints that command's one-line description.
14389
15083
 
14390
- install-skill [--user | --project] [--margaui-skill | --immutable-skill | --all] [--dot-agents] [--force]
15084
+ feedback [message]
15085
+ Append a feedback record (one JSON object per line) to
15086
+ ~/.tutuca/feedback.jsonl. Use for bugs, confusing messages,
15087
+ or suggestions about the CLI, skills, docs, or the library.
15088
+ Message comes from the positional arg or piped stdin.
15089
+
15090
+ agent-context
15091
+ Print a machine-readable schema of every command, flag, exit code,
15092
+ and error code as JSON on stdout. Use this once to teach an agent the
15093
+ shape of the CLI; the schema is versioned (schemaVersion field).
15094
+
15095
+ install-skill [--user | --project] [--margaui-skill | --immutable-skill | --all] [--dot-agents] [--dry-run] [--force]
14391
15096
  Copy bundled Claude Code skill assets into .claude/skills/<name>/.
14392
15097
  Scope: --project (cwd, default) or --user (~/.claude/skills/).
14393
15098
  Selection (default is the tutuca skill):
@@ -14395,9 +15100,13 @@ COMMANDS (no module required)
14395
15100
  --immutable-skill install the immutable-js skill instead
14396
15101
  --all install every bundled skill
14397
15102
  --dot-agents installs into .agents/skills/ instead of .claude/skills/.
15103
+ --dry-run prints the files that would be written without touching disk.
14398
15104
  --force overwrites existing files.
14399
15105
 
14400
15106
  GLOBAL FLAGS
15107
+ --json Shorthand for \`--format=json\`. Recommended for
15108
+ agent/script consumers — error envelopes are
15109
+ also emitted as JSON on stderr.
14401
15110
  -f, --format <cli|md|json|html>
14402
15111
  Output format. Defaults per command:
14403
15112
  info, list, examples, lint -> cli
@@ -14419,44 +15128,56 @@ EXIT CODES
14419
15128
  3 render crash
14420
15129
  4 test failures
14421
15130
 
15131
+ ERROR FORMAT
15132
+ Diagnostics go to stderr; structured output goes to stdout. Under
15133
+ \`--json\`, errors are emitted as a single-line JSON envelope on stderr:
15134
+ {"error":{"code":"ERR_...","message":"...","suggestion":{...},"hint":"..."}}
15135
+ Errors include "did you mean" suggestions for unknown commands and flags
15136
+ in the same shape as lint suggestions.
15137
+
14422
15138
  EXAMPLES
14423
15139
  # Inspect a module
14424
- tutuca ./src/components.js info
15140
+ tutuca get ./src/components.js
14425
15141
 
14426
15142
  # Machine-readable docs for one component
14427
- tutuca ./src/components.js docs Button -f json -o docs/button.json
15143
+ tutuca show ./src/components.js Button --json -o docs/button.json
14428
15144
 
14429
15145
  # Render every example, pretty-printed HTML to a file
14430
- tutuca ./src/components.js render -f html --pretty -o out/examples.html
15146
+ tutuca render ./src/components.js -f html --pretty -o out/examples.html
14431
15147
 
14432
15148
  # Render a single example
14433
- tutuca ./src/components.js render Button --title "Disabled state"
15149
+ tutuca render ./src/components.js Button --title "Disabled state"
14434
15150
 
14435
15151
  # Post-edit verification: lint, then render the example you changed
14436
- tutuca ./src/components.js lint
14437
- tutuca ./src/components.js render --title "Disabled state"
15152
+ tutuca lint ./src/components.js
15153
+ tutuca render ./src/components.js --title "Disabled state"
14438
15154
  `;
14439
- async function run2(argv) {
15155
+ async function run4(argv, opts = {}) {
14440
15156
  const target = argv?.[0];
14441
15157
  if (!target) {
14442
15158
  process.stdout.write(OVERVIEW);
14443
15159
  return;
14444
15160
  }
14445
15161
  if (target === "help") {
14446
- process.stdout.write(`help: ${describe2}
15162
+ process.stdout.write(`help: ${describe4}
14447
15163
  `);
14448
15164
  return;
14449
15165
  }
14450
15166
  const { COMMANDS: COMMANDS2 } = await Promise.resolve().then(() => (init__registry(), exports__registry));
14451
15167
  const noModule = {
15168
+ feedback: await Promise.resolve().then(() => (init_feedback(), exports_feedback)),
14452
15169
  "install-skill": await Promise.resolve().then(() => (init_install_skill(), exports_install_skill))
14453
15170
  };
14454
15171
  const cmd = COMMANDS2[target] ?? noModule[target];
14455
15172
  if (!cmd) {
14456
- process.stderr.write(`tutuca: unknown command: ${target}
14457
- `);
14458
- process.stderr.write("Run `tutuca help` for the full reference.\n");
14459
- process.exit(1);
15173
+ const { CODES: CODES2, didYouMean: didYouMean2, emitError: emitError2 } = await Promise.resolve().then(() => (init_errors(), exports_errors));
15174
+ const known = [...Object.keys(COMMANDS2), ...Object.keys(noModule), "help"];
15175
+ emitError2(opts, {
15176
+ code: CODES2.USAGE_UNKNOWN_COMMAND,
15177
+ message: `Unknown command '${target}'`,
15178
+ suggestion: didYouMean2(target, known),
15179
+ hint: "Run `tutuca help` for the full reference."
15180
+ });
14460
15181
  }
14461
15182
  process.stdout.write(`${target}: ${cmd.describe}
14462
15183
  `);
@@ -14465,299 +15186,82 @@ async function run2(argv) {
14465
15186
 
14466
15187
  // tools/tutuca.js
14467
15188
  init_install_skill();
15189
+ init_errors();
14468
15190
 
14469
15191
  // tools/cli/with-module.js
14470
- import { parseArgs as parseArgs2 } from "node:util";
14471
-
14472
- // tools/cli/env.js
14473
- init_anode();
14474
- init_lint_check();
14475
- import { JSDOM, VirtualConsole } from "jsdom";
14476
- async function createNodeEnv() {
14477
- const virtualConsole = new VirtualConsole;
14478
- virtualConsole.forwardTo(console, { jsdomErrors: "none" });
14479
- virtualConsole.on("jsdomError", (err) => {
14480
- if (err?.message?.includes("Could not parse CSS stylesheet"))
14481
- return;
14482
- console.error(err.message);
14483
- });
14484
- const dom = new JSDOM("<!DOCTYPE html><html><head></head><body></body></html>", {
14485
- virtualConsole
14486
- });
14487
- const { document: document2, Text, Comment } = dom.window;
14488
- globalThis.document = document2;
14489
-
14490
- class HeadlessParseContext extends ParseContext {
14491
- constructor() {
14492
- super(document2, Text, Comment);
14493
- }
14494
- }
14495
-
14496
- class HeadlessLintParseContext extends LintParseContext {
14497
- constructor() {
14498
- super(document2, Text, Comment);
14499
- }
14500
- }
14501
- return {
14502
- document: dom.window.document,
14503
- ParseContext: HeadlessParseContext,
14504
- LintParseContext: HeadlessLintParseContext
14505
- };
14506
- }
14507
-
14508
- // tools/cli/load.js
14509
- import { resolve as resolve2 } from "node:path";
14510
- async function loadAndNormalize(modulePath) {
14511
- const abs = resolve2(modulePath);
14512
- const mod = await import(abs);
14513
- const { normalized } = normalizeModule(mod, { path: abs });
14514
- return normalized;
14515
- }
14516
-
14517
- // tools/cli/output.js
14518
- import { writeFileSync } from "node:fs";
14519
-
14520
- // tools/format/cli.js
14521
- var exports_cli = {};
14522
- __export(exports_cli, {
14523
- supports: () => supports,
14524
- format: () => format
14525
- });
14526
-
14527
- // tools/format/_dispatch.js
14528
- function makeFormatter(name, table) {
14529
- return {
14530
- supports: new Set(Object.keys(table)),
14531
- async format(result, opts) {
14532
- const fn = table[result.constructor.name];
14533
- if (!fn) {
14534
- throw new Error(`${name} formatter missing dispatch for ${result.constructor.name}`);
14535
- }
14536
- return await fn(result, opts);
14537
- }
14538
- };
14539
- }
14540
-
14541
- // tools/format/lint.js
14542
- var UNSUPPORTED_EXPR_LABEL = {
14543
- ternary: "ternary expression",
14544
- comparison: "comparison",
14545
- logical: "logical expression",
14546
- "call-with-args": "method call with arguments"
14547
- };
14548
- function unsupportedExprMessage(info) {
14549
- const v = JSON.stringify(info.value);
14550
- const label = UNSUPPORTED_EXPR_LABEL[info.detected] ?? "expression";
14551
- switch (info.role) {
14552
- case "attr":
14553
- return `Unsupported ${label} ${v} in dynamic attribute ':${info.attr}'`;
14554
- case "directive":
14555
- return `Unsupported ${label} ${v} in directive '@${info.directive}'`;
14556
- case "if":
14557
- return `Unsupported ${label} ${v} in '@if.${info.attr}' condition`;
14558
- case "x-op":
14559
- return `Unsupported ${label} ${v} in <x ${info.op}>`;
14560
- default:
14561
- return `Unsupported ${label} ${v}`;
14562
- }
14563
- }
14564
- function badValueMessage(info) {
14565
- const v = JSON.stringify(info.value);
14566
- switch (info.role) {
14567
- case "attr":
14568
- return `Cannot parse value ${v} for attribute ':${info.attr}'`;
14569
- case "directive":
14570
- return `Cannot parse value ${v} for directive '@${info.directive}'`;
14571
- case "if":
14572
- return `Cannot parse condition ${v} for '@if.${info.attr}'`;
14573
- case "x-op":
14574
- return `Cannot parse value ${v} for <x ${info.op}>`;
14575
- case "handler-name":
14576
- return `Cannot parse handler name ${v}`;
14577
- case "handler-arg":
14578
- return `Cannot parse handler argument ${v}`;
14579
- case "macro-var":
14580
- return `Macro variable '^${info.name}' is not defined`;
14581
- default:
14582
- return `Cannot parse value ${v}`;
14583
- }
14584
- }
14585
- function tagDisplay(tag) {
14586
- return tag ? String(tag).toLowerCase() : null;
14587
- }
14588
- function fmtTagSuffix(info) {
14589
- const t = tagDisplay(info?.tag);
14590
- return t && t !== "x" ? ` on <${t}>` : "";
14591
- }
14592
- function fmtOriginSuffix(info) {
14593
- if (!info)
14594
- return "";
14595
- const parts = [];
14596
- if (info.originAttr) {
14597
- const branch = info.branch ? `[${info.branch}]` : "";
14598
- parts.push(`in ${info.originAttr}${branch}`);
14599
- }
14600
- if (info.handlerName) {
14601
- parts.push(`handler '${info.handlerName}'${info.argIndex !== undefined ? ` arg ${info.argIndex}` : ""}`);
14602
- }
14603
- const t = tagDisplay(info.tag);
14604
- if (t && t !== "x")
14605
- parts.push(`on <${t}>`);
14606
- return parts.length ? ` (${parts.join(", ")})` : "";
14607
- }
14608
- function fmtEventSuffix(info) {
14609
- if (info?.originAttr)
14610
- return ` in ${info.originAttr}`;
14611
- if (info?.eventName)
14612
- return ` in @on.${info.eventName}`;
14613
- return "";
14614
- }
14615
- function lintIdToMessage(id, info) {
14616
- switch (id) {
14617
- case "RENDER_IT_OUTSIDE_OF_LOOP":
14618
- return "<x render-it> used outside of a loop";
14619
- case "UNKNOWN_EVENT_MODIFIER": {
14620
- const mods = info.handler?.modifiers ?? [info.modifier];
14621
- const written = `@on.${info.name}+${mods.join("+")}`;
14622
- return `Unknown modifier '+${info.modifier}' in '${written}'`;
14623
- }
14624
- case "UNKNOWN_HANDLER_ARG_NAME":
14625
- return `Unknown handler argument '${info.name}'${fmtOriginSuffix(info)}`;
14626
- case "INPUT_HANDLER_NOT_IMPLEMENTED":
14627
- return `Input handler '${info.name}' is not implemented${fmtEventSuffix(info)}`;
14628
- case "INPUT_HANDLER_NOT_REFERENCED":
14629
- return `Input handler '${info.name}' is defined but never used — remove it or wire it to an @on.* event`;
14630
- case "INPUT_HANDLER_METHOD_NOT_IMPLEMENTED":
14631
- return `Method '.${info.name}' is not implemented${fmtEventSuffix(info)}`;
14632
- case "INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD":
14633
- return `'.${info.name}' is a method reference, but '${info.name}' is defined as an input handler${fmtEventSuffix(info)}`;
14634
- case "INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER":
14635
- return `'${info.name}' is an input handler reference, but '${info.name}' is defined as a method${fmtEventSuffix(info)}`;
14636
- case "FIELD_VAL_NOT_DEFINED":
14637
- return `Field '.${info.name}' is not defined${fmtOriginSuffix(info)}`;
14638
- case "DUPLICATE_ATTR_DEFINITION": {
14639
- const sources = info.sources?.length ? ` (${info.sources.join(", ")})` : "";
14640
- const tag = info.tag ? ` on <${String(info.tag).toLowerCase()}>` : "";
14641
- return `Attribute '${info.name}' is set ${info.sources?.length ?? "multiple"} times${sources}${tag}`;
14642
- }
14643
- case "IF_NO_BRANCH_SET":
14644
- return `'@if.${info.attr}' has no '@then' or '@else' branch — add '@then="…"' or '@else="…"' (or both)${fmtTagSuffix(info)}`;
14645
- case "UNKNOWN_REQUEST_NAME":
14646
- return `Unknown request '!${info.name}'${fmtOriginSuffix(info)}`;
14647
- case "UNKNOWN_COMPONENT_NAME":
14648
- return `Unknown component '${info.name}'${fmtOriginSuffix(info)}`;
14649
- case "ALT_HANDLER_NOT_DEFINED":
14650
- return `Alter handler '${info.name}' is not defined${fmtOriginSuffix(info)}`;
14651
- case "ALT_HANDLER_NOT_REFERENCED":
14652
- return `Alter handler '${info.name}' is defined but never used — remove it or reference it from @when, @enrich-with, or @loop-with`;
14653
- case "UNKNOWN_MACRO_ARG":
14654
- return `Argument '${info.name}' is not declared in macro '${info.macroName}'`;
14655
- case "UNKNOWN_DIRECTIVE":
14656
- return `Unknown directive '@${info.name}=${JSON.stringify(info.value)}'${fmtTagSuffix(info)}`;
14657
- case "UNKNOWN_X_OP":
14658
- return `Unknown <x> op '${info.name}=${JSON.stringify(info.value)}'${fmtTagSuffix(info)}`;
14659
- case "UNKNOWN_X_ATTR":
14660
- return `Unknown attribute '${info.name}=${JSON.stringify(info.value)}' on <x ${info.op}>${fmtTagSuffix(info)}`;
14661
- case "MAYBE_DROP_AT_PREFIX": {
14662
- const written = info.value !== undefined ? `${info.name}=${JSON.stringify(info.value)}` : info.name;
14663
- return `'${written}' on <x> looks like a directive but is actually an x op/attr written with a leading '@'`;
14664
- }
14665
- case "BAD_VALUE":
14666
- return `${badValueMessage(info)}${fmtTagSuffix(info)}`;
14667
- case "UNSUPPORTED_EXPR_SYNTAX":
14668
- return `${unsupportedExprMessage(info)}${fmtTagSuffix(info)}`;
14669
- case "REDUNDANT_TEMPLATE_STRING":
14670
- return `Redundant template string — '{${info.simpler}}' should be just '${info.simpler}'${fmtOriginSuffix(info)}`;
14671
- case "UNKNOWN_COMPONENT_SPEC_KEY":
14672
- return `Unknown component spec key '${info.key}' — value will be ignored at runtime`;
14673
- case "HTML_TAG_NAME_HAS_UPPERCASE":
14674
- return `Tag <${info.raw}> will be lowercased to <${info.lowercased}>${fmtLocationSuffix(info)}`;
14675
- case "HTML_SVG_TAG_WILL_LOWERCASE":
14676
- return `SVG tag <${info.raw}> is not in the WHATWG case-correction list — will be lowercased to <${info.lowercased}>${fmtLocationSuffix(info)}`;
14677
- case "HTML_TAG_NOT_ALLOWED_IN_PARENT":
14678
- return `<${info.tag}> not allowed under <${info.parent ?? "?"}> in ${info.mode} — ${htmlActionPhrase(info.action, info.tag, info.parent)}${fmtLocationSuffix(info)}`;
14679
- case "HTML_TEXT_NOT_ALLOWED_IN_PARENT":
14680
- return `Non-whitespace text not allowed in ${info.mode}: ${JSON.stringify(info.snippet)}${fmtLocationSuffix(info)}`;
14681
- case "HTML_VOID_ELEMENT_HAS_CLOSE_TAG":
14682
- return `Void element <${info.tag}> has an explicit close tag${fmtLocationSuffix(info)}`;
14683
- case "HTML_DUPLICATE_FORM":
14684
- return `Nested <form> — the inner form will be dropped by the parser${fmtLocationSuffix(info)}`;
14685
- case "HTML_NESTED_INTERACTIVE":
14686
- return `<${info.tag}> nested inside another <${info.tag}> — adoption agency will reorder${fmtLocationSuffix(info)}`;
14687
- case "HTML_MISNESTED_FORMATTING":
14688
- return `Misnested formatting tag </${info.tag}> — adoption agency will reorder nodes${fmtLocationSuffix(info)}`;
14689
- case "HTML_UNEXPECTED_END_TAG":
14690
- return `Unexpected end tag </${info.tag}>${fmtLocationSuffix(info)}`;
14691
- case "HTML_UNCLOSED_BEFORE_END":
14692
- return `<${info.unclosed}> still open when </${info.tag}> was seen — implicitly closed${fmtLocationSuffix(info)}`;
14693
- case "HTML_DUPLICATE_ATTRIBUTE":
14694
- return `Duplicate attribute '${info.name}' — second occurrence dropped${fmtLocationSuffix(info)}`;
14695
- case "HTML_ATTRIBUTES_ON_END_TAG":
14696
- return `Attributes on end tag </${info.tag}> — dropped by the parser${fmtLocationSuffix(info)}`;
14697
- case "HTML_SELF_CLOSING_END_TAG":
14698
- return `Self-closing end tag </${info.tag}/> — trailing '/' is meaningless${fmtLocationSuffix(info)}`;
14699
- case "HTML_MISSING_ATTRIBUTE_VALUE":
14700
- return `Attribute '${info.name}' is missing a value${fmtLocationSuffix(info)}`;
14701
- case "HTML_CDATA_IN_HTML_NAMESPACE":
14702
- return `CDATA section in HTML namespace — reinterpreted as a bogus comment${fmtLocationSuffix(info)}`;
14703
- case "HTML_BOGUS_COMMENT":
14704
- return `Bogus comment — content dropped by the parser${fmtLocationSuffix(info)}`;
14705
- case "HTML_SVG_ATTR_WILL_LOWERCASE":
14706
- return `SVG attribute '${info.raw}' will be rewritten to '${info.canonical}'${fmtLocationSuffix(info)}`;
14707
- case "HTML_MATHML_ATTR_WILL_LOWERCASE":
14708
- return `MathML attribute '${info.raw}' will be rewritten to '${info.canonical}'${fmtLocationSuffix(info)}`;
14709
- case "LINT_ERROR":
14710
- return info.message;
14711
- default:
14712
- return id;
15192
+ import { parseArgs as parseArgs3 } from "node:util";
15193
+
15194
+ // tools/cli/env.js
15195
+ init_anode();
15196
+ init_lint_check();
15197
+ import { JSDOM, VirtualConsole } from "jsdom";
15198
+ async function createNodeEnv() {
15199
+ const virtualConsole = new VirtualConsole;
15200
+ virtualConsole.forwardTo(console, { jsdomErrors: "none" });
15201
+ virtualConsole.on("jsdomError", (err) => {
15202
+ if (err?.message?.includes("Could not parse CSS stylesheet"))
15203
+ return;
15204
+ console.error(err.message);
15205
+ });
15206
+ const dom = new JSDOM("<!DOCTYPE html><html><head></head><body></body></html>", {
15207
+ virtualConsole
15208
+ });
15209
+ const { document: document2, Text, Comment } = dom.window;
15210
+ globalThis.document = document2;
15211
+
15212
+ class HeadlessParseContext extends ParseContext {
15213
+ constructor() {
15214
+ super(document2, Text, Comment);
15215
+ }
14713
15216
  }
14714
- }
14715
- function suggestionToMessage(suggestion) {
14716
- if (!suggestion)
14717
- return null;
14718
- switch (suggestion.kind) {
14719
- case "replace-name":
14720
- return `did you mean '${suggestion.to}'?`;
14721
- case "drop-prefix":
14722
- return `did you mean '${suggestion.to}'? (drop the leading '${suggestion.from.slice(0, suggestion.from.length - suggestion.to.length)}')`;
14723
- case "add-prefix":
14724
- return `did you mean '${suggestion.to}'? (add the leading '${suggestion.to.slice(0, suggestion.to.length - suggestion.from.length)}')`;
14725
- case "remove":
14726
- return `remove ${suggestion.what}`;
14727
- case "rewrite":
14728
- return `use '${suggestion.to}' instead of '${suggestion.from}'`;
14729
- case "wrap":
14730
- return `wrap it in ${suggestion.to}`;
14731
- case "rephrase":
14732
- return suggestion.text ?? null;
14733
- default:
14734
- return null;
15217
+
15218
+ class HeadlessLintParseContext extends LintParseContext {
15219
+ constructor() {
15220
+ super(document2, Text, Comment);
15221
+ }
14735
15222
  }
15223
+ return {
15224
+ document: dom.window.document,
15225
+ ParseContext: HeadlessParseContext,
15226
+ LintParseContext: HeadlessLintParseContext
15227
+ };
14736
15228
  }
14737
- function fmtLocationSuffix(info) {
14738
- const loc = info?.location;
14739
- if (!loc)
14740
- return "";
14741
- return ` at line ${loc.line}, col ${loc.column}`;
15229
+
15230
+ // tools/cli/load.js
15231
+ import { resolve as resolve4 } from "node:path";
15232
+ async function loadAndNormalize(modulePath) {
15233
+ const abs = resolve4(modulePath);
15234
+ const mod = await import(abs);
15235
+ const { normalized } = normalizeModule(mod, { path: abs });
15236
+ return normalized;
14742
15237
  }
14743
- function htmlActionPhrase(action, tag, parent) {
14744
- switch (action) {
14745
- case "ignored":
14746
- return `the parser will drop this <${tag}>`;
14747
- case "drop":
14748
- return `the parser will drop this <${tag}>`;
14749
- case "auto-close-implicit":
14750
- return `the parser will close <${parent ?? "?"}> first, then place <${tag}> as a sibling`;
14751
- case "foster-parent":
14752
- return `the parser will move <${tag}> outside <${parent ?? "?"}> (foster-parenting)`;
14753
- case "foreign-breakout":
14754
- return `the parser will exit foreign content and re-process <${tag}> in HTML mode`;
14755
- default:
14756
- return `parser action: ${action}`;
14757
- }
15238
+
15239
+ // tools/cli/output.js
15240
+ import { writeFileSync } from "node:fs";
15241
+
15242
+ // tools/format/cli.js
15243
+ var exports_cli = {};
15244
+ __export(exports_cli, {
15245
+ supports: () => supports,
15246
+ format: () => format
15247
+ });
15248
+
15249
+ // tools/format/_dispatch.js
15250
+ function makeFormatter(name, table) {
15251
+ return {
15252
+ supports: new Set(Object.keys(table)),
15253
+ async format(result, opts) {
15254
+ const fn = table[result.constructor.name];
15255
+ if (!fn) {
15256
+ throw new Error(`${name} formatter missing dispatch for ${result.constructor.name}`);
15257
+ }
15258
+ return await fn(result, opts);
15259
+ }
15260
+ };
14758
15261
  }
14759
15262
 
14760
15263
  // tools/format/cli.js
15264
+ init_lint2();
14761
15265
  function fmtModuleInfo(info) {
14762
15266
  const lines = [];
14763
15267
  lines.push(`Module: ${info.path ?? "<in-memory>"}`);
@@ -14785,6 +15289,10 @@ function fmtComponentList(list) {
14785
15289
  for (const f of c.fields)
14786
15290
  lines.push(` - ${f.name}: ${f.type}`);
14787
15291
  }
15292
+ if (list.truncated) {
15293
+ const remaining = list.total - list.items.length;
15294
+ lines.push(`… ${remaining} more component(s) — re-run with \`--limit 0\` for all, or pass [name] to filter.`);
15295
+ }
14788
15296
  return lines.join(`
14789
15297
  `);
14790
15298
  }
@@ -14792,12 +15300,18 @@ function fmtExampleIndex(idx) {
14792
15300
  if (idx.sections.length === 0)
14793
15301
  return "(no examples)";
14794
15302
  const lines = [];
15303
+ let shown = 0;
14795
15304
  for (const s of idx.sections) {
14796
15305
  lines.push(`${s.title}${s.description ? ` — ${s.description}` : ""}`);
14797
15306
  for (const item of s.items) {
14798
15307
  lines.push(` - ${item.title}${item.description ? ` — ${item.description}` : ""} [${item.componentName}${item.view !== "main" ? `/${item.view}` : ""}]`);
15308
+ shown += 1;
14799
15309
  }
14800
15310
  }
15311
+ if (idx.truncated) {
15312
+ const remaining = idx.total - shown;
15313
+ lines.push(`… ${remaining} more example(s) — re-run with \`--limit 0\` for all.`);
15314
+ }
14801
15315
  return lines.join(`
14802
15316
  `);
14803
15317
  }
@@ -14919,12 +15433,70 @@ var { supports, format } = makeFormatter("cli", {
14919
15433
  TestReport: fmtTestReport
14920
15434
  });
14921
15435
 
15436
+ // tools/format/html.js
15437
+ var exports_html = {};
15438
+ __export(exports_html, {
15439
+ supports: () => supports2,
15440
+ format: () => format2
15441
+ });
15442
+ async function fmtRenderBatch2(result, { pretty = false } = {}) {
15443
+ const prettify = pretty ? (await import("prettier")).format : null;
15444
+ const parts = [];
15445
+ for (const section of result.sections) {
15446
+ parts.push(`<!-- === ${section.title} === -->`);
15447
+ for (const item of section.items) {
15448
+ if (item.error) {
15449
+ parts.push(`<!-- ERROR ${item.title}: ${item.error.message} -->`);
15450
+ continue;
15451
+ }
15452
+ let html = item.html;
15453
+ if (prettify) {
15454
+ try {
15455
+ html = (await prettify(html, { parser: "html" })).trimEnd();
15456
+ } catch {}
15457
+ }
15458
+ parts.push(`<!-- ${item.title} -->`);
15459
+ parts.push(html);
15460
+ }
15461
+ }
15462
+ return parts.join(`
15463
+ `);
15464
+ }
15465
+ var { supports: supports2, format: format2 } = makeFormatter("html", {
15466
+ RenderBatch: fmtRenderBatch2
15467
+ });
15468
+
15469
+ // tools/format/json.js
15470
+ var exports_json = {};
15471
+ __export(exports_json, {
15472
+ supports: () => supports3,
15473
+ format: () => format3
15474
+ });
15475
+ function replacer(_key, v) {
15476
+ if (v instanceof Set)
15477
+ return [...v];
15478
+ return v;
15479
+ }
15480
+ function fmtJson(result, { pretty = false } = {}) {
15481
+ return JSON.stringify(result, replacer, pretty ? 2 : 0);
15482
+ }
15483
+ var { supports: supports3, format: format3 } = makeFormatter("json", {
15484
+ ModuleInfo: fmtJson,
15485
+ ComponentList: fmtJson,
15486
+ ExampleIndex: fmtJson,
15487
+ ComponentDocs: fmtJson,
15488
+ LintReport: fmtJson,
15489
+ RenderBatch: fmtJson,
15490
+ TestReport: fmtJson
15491
+ });
15492
+
14922
15493
  // tools/format/md.js
14923
15494
  var exports_md = {};
14924
15495
  __export(exports_md, {
14925
- supports: () => supports2,
14926
- format: () => format2
15496
+ supports: () => supports4,
15497
+ format: () => format4
14927
15498
  });
15499
+ init_lint2();
14928
15500
  function fmtComponentDocs2(docs) {
14929
15501
  const lines = [];
14930
15502
  for (const comp of docs.items) {
@@ -14958,7 +15530,7 @@ function fmtComponentDocs2(docs) {
14958
15530
  return lines.join(`
14959
15531
  `);
14960
15532
  }
14961
- async function fmtRenderBatch2(batch, { pretty = false } = {}) {
15533
+ async function fmtRenderBatch3(batch, { pretty = false } = {}) {
14962
15534
  const prettify = pretty ? (await import("prettier")).format : null;
14963
15535
  const lines = [];
14964
15536
  for (const section of batch.sections) {
@@ -15105,9 +15677,9 @@ function fmtTestReport2(report) {
15105
15677
  return lines.join(`
15106
15678
  `);
15107
15679
  }
15108
- var { supports: supports2, format: format2 } = makeFormatter("md", {
15680
+ var { supports: supports4, format: format4 } = makeFormatter("md", {
15109
15681
  ComponentDocs: fmtComponentDocs2,
15110
- RenderBatch: fmtRenderBatch2,
15682
+ RenderBatch: fmtRenderBatch3,
15111
15683
  ExampleIndex: fmtExampleIndex2,
15112
15684
  LintReport: fmtLintReport2,
15113
15685
  ModuleInfo: fmtModuleInfo2,
@@ -15115,77 +15687,34 @@ var { supports: supports2, format: format2 } = makeFormatter("md", {
15115
15687
  TestReport: fmtTestReport2
15116
15688
  });
15117
15689
 
15118
- // tools/format/json.js
15119
- var exports_json = {};
15120
- __export(exports_json, {
15121
- supports: () => supports3,
15122
- format: () => format3
15123
- });
15124
- function replacer(_key, v) {
15125
- if (v instanceof Set)
15126
- return [...v];
15127
- return v;
15128
- }
15129
- function fmtJson(result, { pretty = false } = {}) {
15130
- return JSON.stringify(result, replacer, pretty ? 2 : 0);
15131
- }
15132
- var { supports: supports3, format: format3 } = makeFormatter("json", {
15133
- ModuleInfo: fmtJson,
15134
- ComponentList: fmtJson,
15135
- ExampleIndex: fmtJson,
15136
- ComponentDocs: fmtJson,
15137
- LintReport: fmtJson,
15138
- RenderBatch: fmtJson,
15139
- TestReport: fmtJson
15140
- });
15141
-
15142
- // tools/format/html.js
15143
- var exports_html = {};
15144
- __export(exports_html, {
15145
- supports: () => supports4,
15146
- format: () => format4
15147
- });
15148
- async function fmtRenderBatch3(result, { pretty = false } = {}) {
15149
- const prettify = pretty ? (await import("prettier")).format : null;
15150
- const parts = [];
15151
- for (const section of result.sections) {
15152
- parts.push(`<!-- === ${section.title} === -->`);
15153
- for (const item of section.items) {
15154
- if (item.error) {
15155
- parts.push(`<!-- ERROR ${item.title}: ${item.error.message} -->`);
15156
- continue;
15157
- }
15158
- let html = item.html;
15159
- if (prettify) {
15160
- try {
15161
- html = (await prettify(html, { parser: "html" })).trimEnd();
15162
- } catch {}
15163
- }
15164
- parts.push(`<!-- ${item.title} -->`);
15165
- parts.push(html);
15166
- }
15167
- }
15168
- return parts.join(`
15169
- `);
15170
- }
15171
- var { supports: supports4, format: format4 } = makeFormatter("html", {
15172
- RenderBatch: fmtRenderBatch3
15173
- });
15174
-
15175
15690
  // tools/format/index.js
15176
15691
  var FORMATTERS = { cli: exports_cli, md: exports_md, json: exports_json, html: exports_html };
15177
15692
  function pickFormatter(name) {
15178
15693
  const f = FORMATTERS[name];
15179
15694
  if (!f) {
15180
- throw new Error(`Unknown format: ${name}. Available: ${Object.keys(FORMATTERS).join(", ")}`);
15695
+ const err = new Error(`Unknown format '${name}'. Valid formats: ${Object.keys(FORMATTERS).join(", ")}`);
15696
+ err.code = "ERR_FORMAT_UNKNOWN";
15697
+ err.formatName = name;
15698
+ err.validFormats = Object.keys(FORMATTERS);
15699
+ throw err;
15181
15700
  }
15182
15701
  return f;
15183
15702
  }
15703
+ function formattersSupporting(kind) {
15704
+ return Object.entries(FORMATTERS).filter(([, f]) => f.supports?.has(kind)).map(([name]) => name);
15705
+ }
15184
15706
  async function formatResult(formatName, result, options = {}) {
15185
15707
  const f = pickFormatter(formatName);
15186
15708
  const kind = result.constructor.name;
15187
15709
  if (!f.supports?.has(kind)) {
15188
- throw new Error(`Formatter "${formatName}" does not support ${kind}`);
15710
+ const supported = formattersSupporting(kind);
15711
+ const supportedTail = supported.length ? ` Use one of: ${supported.join(", ")}.` : "";
15712
+ const err = new Error(`Format '${formatName}' does not support ${kind}.${supportedTail}`);
15713
+ err.code = "ERR_FORMAT_UNSUPPORTED";
15714
+ err.formatName = formatName;
15715
+ err.resultKind = kind;
15716
+ err.supportedFormats = supported;
15717
+ throw err;
15189
15718
  }
15190
15719
  return await f.format(result, options);
15191
15720
  }
@@ -15206,7 +15735,7 @@ async function emit(result, { format: format5, pretty, output }) {
15206
15735
 
15207
15736
  // tools/cli/with-module.js
15208
15737
  async function runCommand(cmd, argv, globalOpts) {
15209
- const parsed = parseArgs2({
15738
+ const parsed = parseArgs3({
15210
15739
  args: argv,
15211
15740
  options: cmd.parseOptions ?? {},
15212
15741
  allowPositionals: true
@@ -15229,23 +15758,29 @@ async function runCommand(cmd, argv, globalOpts) {
15229
15758
  // tools/tutuca.js
15230
15759
  var NO_MODULE_COMMANDS = {
15231
15760
  help: exports_help,
15232
- "install-skill": exports_install_skill
15761
+ feedback: exports_feedback,
15762
+ "install-skill": exports_install_skill,
15763
+ "agent-context": exports_agent_context
15233
15764
  };
15234
- function usageError(msg) {
15235
- process.stderr.write(`tutuca: ${msg}
15236
- Run \`tutuca help\` for usage.
15237
- `);
15238
- process.exit(1);
15239
- }
15765
+ var VALID_FORMATS = ["cli", "md", "json", "html"];
15766
+ var GLOBAL_FLAGS2 = ["format", "output", "pretty", "module", "json", "help", "no-color"];
15240
15767
  function extractGlobals(argv) {
15241
15768
  const rest = [];
15242
- const opts = { format: null, output: null, pretty: false, module: null, help: false };
15769
+ const opts = {
15770
+ format: null,
15771
+ output: null,
15772
+ pretty: false,
15773
+ module: null,
15774
+ help: false
15775
+ };
15243
15776
  for (let i = 0;i < argv.length; i++) {
15244
15777
  const a = argv[i];
15245
15778
  if (a === "-f" || a === "--format") {
15246
15779
  opts.format = argv[++i];
15247
15780
  } else if (a.startsWith("--format=")) {
15248
15781
  opts.format = a.slice("--format=".length);
15782
+ } else if (a === "--json") {
15783
+ opts.format = "json";
15249
15784
  } else if (a === "-o" || a === "--output") {
15250
15785
  opts.output = argv[++i];
15251
15786
  } else if (a.startsWith("--output=")) {
@@ -15262,41 +15797,59 @@ function extractGlobals(argv) {
15262
15797
  rest.push(a);
15263
15798
  }
15264
15799
  }
15800
+ if (opts.format != null && !VALID_FORMATS.includes(opts.format)) {
15801
+ emitError(opts, {
15802
+ code: CODES.FORMAT_UNKNOWN,
15803
+ message: `Unknown format '${opts.format}'`,
15804
+ suggestion: didYouMean(opts.format, VALID_FORMATS),
15805
+ hint: `Valid formats: ${VALID_FORMATS.join(", ")}`
15806
+ });
15807
+ }
15265
15808
  return { opts, rest };
15266
15809
  }
15810
+ function dispatchKnownCommands() {
15811
+ return [...Object.keys(COMMANDS), ...Object.keys(NO_MODULE_COMMANDS)];
15812
+ }
15267
15813
  async function main() {
15268
15814
  const { opts, rest } = extractGlobals(process.argv.slice(2));
15269
15815
  if (rest.length === 0 || opts.help && rest.length === 0) {
15270
- await run2([], opts);
15816
+ await run4([], opts);
15271
15817
  return;
15272
15818
  }
15273
- let command;
15274
- let commandArgs;
15275
- if (NO_MODULE_COMMANDS[rest[0]]) {
15276
- command = rest[0];
15277
- commandArgs = rest.slice(1);
15278
- } else if (opts.module) {
15279
- command = rest[0];
15280
- commandArgs = rest.slice(1);
15281
- } else {
15282
- if (rest.length < 2)
15283
- return usageError("expected <module-path> <command>");
15284
- opts.module = rest[0];
15285
- command = rest[1];
15286
- commandArgs = rest.slice(2);
15287
- }
15819
+ const command = rest[0];
15288
15820
  if (NO_MODULE_COMMANDS[command]) {
15289
- const args = opts.help ? [...commandArgs, "--help"] : commandArgs;
15821
+ const commandArgs2 = rest.slice(1);
15822
+ const args = opts.help ? [...commandArgs2, "--help"] : commandArgs2;
15290
15823
  await NO_MODULE_COMMANDS[command].run(args, opts);
15291
15824
  return;
15292
15825
  }
15293
15826
  if (opts.help) {
15294
- await run2([command], opts);
15827
+ await run4([command], opts);
15295
15828
  return;
15296
15829
  }
15297
15830
  const cmd = COMMANDS[command];
15298
- if (!cmd)
15299
- return usageError(`unknown command: ${command}`);
15831
+ if (!cmd) {
15832
+ const known = dispatchKnownCommands();
15833
+ emitError(opts, {
15834
+ code: CODES.USAGE_UNKNOWN_COMMAND,
15835
+ message: `Unknown command '${command}'`,
15836
+ suggestion: didYouMean(command, known),
15837
+ hint: "Run `tutuca help` for the full reference."
15838
+ });
15839
+ }
15840
+ let commandArgs;
15841
+ if (opts.module) {
15842
+ commandArgs = rest.slice(1);
15843
+ } else if (rest.length < 2 || rest[1].startsWith("-")) {
15844
+ emitError(opts, {
15845
+ code: CODES.USAGE_MISSING_MODULE,
15846
+ message: `'${command}' requires a module path`,
15847
+ hint: `Pass it as the second positional: \`tutuca ${command} <module-path>\`, or use --module=<path>.`
15848
+ });
15849
+ } else {
15850
+ opts.module = rest[1];
15851
+ commandArgs = rest.slice(2);
15852
+ }
15300
15853
  try {
15301
15854
  await runCommand(cmd, commandArgs, opts);
15302
15855
  } catch (e) {
@@ -15306,21 +15859,56 @@ async function main() {
15306
15859
  parts.push(opts.module);
15307
15860
  if (e.where)
15308
15861
  parts.push(`@ ${e.where}`);
15309
- const prefix = parts.length ? `[${parts.join(" ")}] ` : "";
15310
- process.stderr.write(`tutuca: ${prefix}${e.message}
15311
- `);
15312
- process.exit(1);
15862
+ emitError(opts, {
15863
+ code: CODES.MODULE_SHAPE_MISMATCH,
15864
+ message: e.message,
15865
+ where: parts.length ? parts.join(" ") : null
15866
+ });
15867
+ }
15868
+ if (e?.code === "ERR_FORMAT_UNKNOWN") {
15869
+ emitError(opts, {
15870
+ code: CODES.FORMAT_UNKNOWN,
15871
+ message: e.message,
15872
+ suggestion: didYouMean(e.formatName, e.validFormats ?? VALID_FORMATS)
15873
+ });
15874
+ }
15875
+ if (e?.code === "ERR_FORMAT_UNSUPPORTED") {
15876
+ emitError(opts, {
15877
+ code: CODES.FORMAT_UNSUPPORTED,
15878
+ message: e.message,
15879
+ suggestion: e.supportedFormats?.length ? { kind: "rewrite", from: e.formatName, to: e.supportedFormats[0] } : null
15880
+ });
15313
15881
  }
15314
15882
  if (e?.code?.startsWith?.("ERR_PARSE_ARGS_")) {
15315
- process.stderr.write(`tutuca: ${e.message}
15316
- `);
15317
- process.exit(1);
15883
+ const validFlags = [...Object.keys(cmd.parseOptions ?? {}), ...GLOBAL_FLAGS2];
15884
+ const shape = parseArgsErrorShape(e, validFlags);
15885
+ emitError(opts, {
15886
+ ...shape,
15887
+ hint: shape.hint ?? `Run \`tutuca help ${command}\` for valid flags.`
15888
+ });
15318
15889
  }
15319
15890
  throw e;
15320
15891
  }
15321
15892
  }
15322
15893
  main().catch((e) => {
15323
- process.stderr.write(`${e.stack ?? e.message ?? e}
15894
+ const wantsJson = process.argv.includes("--json") || process.argv.includes("--format=json") || process.argv.includes("-f=json") || (() => {
15895
+ const i = process.argv.findIndex((a) => a === "-f" || a === "--format");
15896
+ return i >= 0 && process.argv[i + 1] === "json";
15897
+ })();
15898
+ if (wantsJson) {
15899
+ const envelope = {
15900
+ error: {
15901
+ code: e?.code ?? CODES.INTERNAL,
15902
+ message: e?.message ?? String(e)
15903
+ }
15904
+ };
15905
+ if (e?.where)
15906
+ envelope.error.where = e.where;
15907
+ process.stderr.write(`${JSON.stringify(envelope)}
15908
+ `);
15909
+ } else {
15910
+ process.stderr.write(`${e.stack ?? e.message ?? e}
15324
15911
  `);
15912
+ }
15325
15913
  process.exit(1);
15326
15914
  });