@takemo101/mikan 0.0.6 → 0.0.8

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 (3) hide show
  1. package/README.md +3 -1
  2. package/dist/bin.js +419 -164
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  mikan is a tiny local-first Issue board for AI-assisted development. It stores Issues as Markdown files under `.mikan/` and exposes the same board through a CLI, keyboard-first TUI, stdio MCP server, and polling watcher.
4
4
 
5
+ Manual: <https://takemo101.github.io/mikan/>
6
+
5
7
  ## Install
6
8
 
7
9
  ```sh
@@ -17,7 +19,7 @@ mikan is currently built for Bun-based execution. The npm package installs the `
17
19
 
18
20
  - **Markdown source of truth**: each Issue is a file such as `.mikan/ready/MIK-001.md`.
19
21
  - **Primitive CLI commands**: `init`, `add`, `list`, `show`, `update`, `move`, `append`, `github`, `tui`, `watch`, `mcp`, `skills`.
20
- - **Keyboard TUI**: board-first flow with detail view, Note modal, Move shortcuts, and Archive confirmation.
22
+ - **Keyboard TUI**: board-first flow with detail view, Label/Note/Warning modals, Move shortcuts, and Archive confirmation.
21
23
  - **MCP server**: stdio tools for agents: `get_board`, `list_issues`, `get_issue`, `create_issue`, `update_issue`, `move_issue`, `append_issue`, `mirror_issue_to_github`.
22
24
  - **GitHub Mirror**: explicit one-way publication from local Markdown Issues to GitHub Issues.
23
25
  - **Agent setup**: register the MCP server or install agent guidance for common AI agents.
package/dist/bin.js CHANGED
@@ -100301,8 +100301,12 @@ function updateIssue(options) {
100301
100301
  const target = findIssueById(options);
100302
100302
  if (!target.ok)
100303
100303
  return target;
100304
- const labels = options.labels ?? target.value.issue.labels.map(String);
100305
- const labelsValidation = validateLabels(options.config, labels);
100304
+ const existingLabels = target.value.issue.labels.map(String);
100305
+ const configuredLabelIds = new Set(options.config.labels.map((label) => label.id));
100306
+ const existingUnknownLabels = new Set(existingLabels.filter((label) => !configuredLabelIds.has(label)));
100307
+ const labels = options.labels ?? existingLabels;
100308
+ const labelsToValidate = options.preserveUnknownLabels ? labels.filter((label) => !existingUnknownLabels.has(label)) : labels;
100309
+ const labelsValidation = validateLabels(options.config, labelsToValidate);
100306
100310
  if (!labelsValidation.ok)
100307
100311
  return labelsValidation;
100308
100312
  const dependenciesValidation = options.dependencies ? validateDependencies(options.dependencies) : undefined;
@@ -100798,9 +100802,18 @@ var labelSchema = exports_external.object({
100798
100802
  id: nonEmptyString,
100799
100803
  title: nonEmptyString
100800
100804
  });
100805
+ var hookEntrySchema = exports_external.union([
100806
+ nonEmptyString,
100807
+ exports_external.object({
100808
+ command: nonEmptyString,
100809
+ when: exports_external.object({
100810
+ labels_include: exports_external.array(nonEmptyString).min(1).optional()
100811
+ }).optional()
100812
+ })
100813
+ ]);
100801
100814
  var hookSchema = exports_external.object({
100802
- on_enter: exports_external.record(exports_external.string(), exports_external.array(exports_external.string())).optional(),
100803
- on_transition: exports_external.record(exports_external.string(), exports_external.array(exports_external.string())).optional()
100815
+ on_enter: exports_external.record(exports_external.string(), exports_external.array(hookEntrySchema)).optional(),
100816
+ on_transition: exports_external.record(exports_external.string(), exports_external.array(hookEntrySchema)).optional()
100804
100817
  });
100805
100818
  var githubSchema = exports_external.object({
100806
100819
  repo: githubRepoSchema,
@@ -107389,7 +107402,7 @@ var import_react20 = __toESM(require_react(), 1);
107389
107402
  // package.json
107390
107403
  var package_default = {
107391
107404
  name: "@takemo101/mikan",
107392
- version: "0.0.6",
107405
+ version: "0.0.8",
107393
107406
  private: false,
107394
107407
  type: "module",
107395
107408
  bin: {
@@ -107453,13 +107466,16 @@ function visibleDetailLineCount(viewportHeight) {
107453
107466
  return Math.max(1, viewportHeight - 8);
107454
107467
  }
107455
107468
  function footerText(mode) {
107469
+ if (mode === "note-modal") {
107470
+ return "Note | Enter newline | Ctrl+S save | Esc cancel";
107471
+ }
107456
107472
  if (mode === "modal") {
107457
107473
  return "Modal | enter confirm | esc cancel | ? keys";
107458
107474
  }
107459
107475
  if (mode === "detail") {
107460
- return "Detail | \u2191\u2193 scroll | g github | esc board | ? keys";
107476
+ return "Detail | \u2191\u2193 scroll | esc board | ? keys";
107461
107477
  }
107462
- return "Board | \u2191\u2193 card | \u2190\u2192 column | enter detail | g github | ? keys";
107478
+ return "Board | \u2191\u2193 card | \u2190\u2192 column | enter detail | ? keys";
107463
107479
  }
107464
107480
 
107465
107481
  // ../tui/src/selection.ts
@@ -107566,6 +107582,7 @@ function buildTuiModel(board, labels = [], githubRepo) {
107566
107582
  })),
107567
107583
  warnings: board.warnings.map(formatWarning),
107568
107584
  ...board.warnings.length > 0 ? { warningDetails: board.warnings.map(formatTuiWarning) } : {},
107585
+ labels: labels.map((label) => ({ id: label.id, title: label.title })),
107569
107586
  labelTitles: Object.fromEntries(labels.map((label) => [label.id, label.title])),
107570
107587
  githubRepo
107571
107588
  };
@@ -107940,7 +107957,7 @@ function moveSelection(model, selection, direction, options = {}) {
107940
107957
  message: undefined
107941
107958
  };
107942
107959
  }
107943
- if (selection.detailOpen && !selection.moveOpen && !selection.noteOpen) {
107960
+ if (selection.detailOpen && !selection.moveOpen && !selection.noteOpen && !selection.labelOpen) {
107944
107961
  if (direction === "up" || direction === "down") {
107945
107962
  return {
107946
107963
  ...selection,
@@ -107961,6 +107978,9 @@ function moveSelection(model, selection, direction, options = {}) {
107961
107978
  if (selection.githubConfirmOpen) {
107962
107979
  return { ...selection, githubConfirmOpen: false };
107963
107980
  }
107981
+ if (selection.labelOpen) {
107982
+ return { ...selection, labelOpen: false };
107983
+ }
107964
107984
  if (selection.warningsOpen) {
107965
107985
  return { ...selection, warningsOpen: false };
107966
107986
  }
@@ -107968,7 +107988,10 @@ function moveSelection(model, selection, direction, options = {}) {
107968
107988
  ...selection,
107969
107989
  detailOpen: false,
107970
107990
  moveOpen: false,
107971
- noteOpen: false
107991
+ noteOpen: false,
107992
+ noteDraft: undefined,
107993
+ labelOpen: false,
107994
+ message: selection.noteOpen ? undefined : selection.message
107972
107995
  };
107973
107996
  }
107974
107997
  if (direction === "move") {
@@ -107977,6 +108000,8 @@ function moveSelection(model, selection, direction, options = {}) {
107977
108000
  archiveOpen: false,
107978
108001
  detailOpen: false,
107979
108002
  noteOpen: false,
108003
+ noteDraft: undefined,
108004
+ labelOpen: false,
107980
108005
  moveOpen: true,
107981
108006
  moveTargetIndex: 0
107982
108007
  };
@@ -107987,7 +108012,25 @@ function moveSelection(model, selection, direction, options = {}) {
107987
108012
  archiveOpen: false,
107988
108013
  detailOpen: false,
107989
108014
  moveOpen: false,
107990
- noteOpen: true
108015
+ labelOpen: false,
108016
+ noteOpen: true,
108017
+ noteDraft: "",
108018
+ message: undefined
108019
+ };
108020
+ }
108021
+ if (direction === "edit-labels") {
108022
+ const card = model.columns[selection.columnIndex]?.cards[selection.cardIndex];
108023
+ const knownLabelIds = new Set((model.labels ?? []).map((label) => label.id));
108024
+ return {
108025
+ ...selection,
108026
+ archiveOpen: false,
108027
+ githubConfirmOpen: false,
108028
+ moveOpen: false,
108029
+ noteOpen: false,
108030
+ noteDraft: undefined,
108031
+ labelOpen: true,
108032
+ labelFocusIndex: 0,
108033
+ labelDraftIds: card?.labels.filter((label) => knownLabelIds.has(label)) ?? []
107991
108034
  };
107992
108035
  }
107993
108036
  if (direction === "archive") {
@@ -107996,6 +108039,8 @@ function moveSelection(model, selection, direction, options = {}) {
107996
108039
  archiveOpen: true,
107997
108040
  moveOpen: false,
107998
108041
  noteOpen: false,
108042
+ noteDraft: undefined,
108043
+ labelOpen: false,
107999
108044
  githubConfirmOpen: false
108000
108045
  };
108001
108046
  }
@@ -108005,6 +108050,8 @@ function moveSelection(model, selection, direction, options = {}) {
108005
108050
  archiveOpen: false,
108006
108051
  moveOpen: false,
108007
108052
  noteOpen: false,
108053
+ noteDraft: undefined,
108054
+ labelOpen: false,
108008
108055
  githubConfirmOpen: true
108009
108056
  };
108010
108057
  }
@@ -108036,28 +108083,38 @@ function getAdjacentMoveTarget(model, selection, direction) {
108036
108083
  const column = model.columns[selection.columnIndex + offset];
108037
108084
  return column ? { id: column.id, title: column.title } : undefined;
108038
108085
  }
108039
- function applyNoteInput(selection, keyName2, shift = false) {
108040
- if (!selection.noteOpen || !keyName2)
108086
+ function moveLabelFocus(model, selection, direction) {
108087
+ if (!selection.labelOpen)
108041
108088
  return selection;
108042
- if (keyName2 === "backspace") {
108043
- return {
108044
- ...selection,
108045
- noteDraft: (selection.noteDraft ?? "").slice(0, -1)
108046
- };
108047
- }
108048
- const character = keyName2 === "space" ? " " : keyName2;
108049
- if (character.length !== 1)
108089
+ return {
108090
+ ...selection,
108091
+ labelFocusIndex: clamp((selection.labelFocusIndex ?? 0) + (direction === "down" ? 1 : -1), 0, Math.max(0, (model.labels ?? []).length - 1))
108092
+ };
108093
+ }
108094
+ function toggleFocusedLabel(model, selection) {
108095
+ if (!selection.labelOpen)
108096
+ return selection;
108097
+ const label = (model.labels ?? [])[selection.labelFocusIndex ?? 0];
108098
+ if (!label)
108050
108099
  return selection;
108051
- const value = shift && /[a-z]/.test(character) ? character.toUpperCase() : character;
108052
- return { ...selection, noteDraft: `${selection.noteDraft ?? ""}${value}` };
108100
+ const current = new Set(selection.labelDraftIds ?? []);
108101
+ if (current.has(label.id))
108102
+ current.delete(label.id);
108103
+ else
108104
+ current.add(label.id);
108105
+ return { ...selection, labelDraftIds: [...current] };
108053
108106
  }
108054
108107
  function footerMode(selection) {
108055
- if (selection.moveOpen || selection.noteOpen || selection.archiveOpen || selection.githubConfirmOpen) {
108108
+ if (selection.noteOpen)
108109
+ return "note-modal";
108110
+ if (selection.moveOpen || selection.labelOpen || selection.archiveOpen || selection.githubConfirmOpen) {
108056
108111
  return "modal";
108057
108112
  }
108058
108113
  return selection.detailOpen ? "detail" : "board";
108059
108114
  }
108060
- function keyToTuiAction(keyName2, shift = false) {
108115
+ function keyToTuiAction(keyName2, shift = false, ctrl = false) {
108116
+ if (ctrl && keyName2 === "s")
108117
+ return "save-note";
108061
108118
  if (shift && keyName2 === "h")
108062
108119
  return "move-left";
108063
108120
  if (shift && keyName2 === "l")
@@ -108090,6 +108147,8 @@ function keyToTuiAction(keyName2, shift = false) {
108090
108147
  return "move";
108091
108148
  case "n":
108092
108149
  return "append-note";
108150
+ case "e":
108151
+ return "edit-labels";
108093
108152
  case "a":
108094
108153
  return "archive";
108095
108154
  case "g":
@@ -108136,8 +108195,31 @@ function buildNotePromptViewModel(model, selection) {
108136
108195
  return {
108137
108196
  title: `Append note to ${card.id}`,
108138
108197
  focused: Boolean(selection.noteOpen),
108139
- draft: selection.noteDraft ?? "",
108140
- hint: "enter append esc cancel"
108198
+ feedback: selection.message === "Note cannot be empty" ? selection.message : undefined,
108199
+ hint: "Enter newline | Ctrl+S save | Esc cancel"
108200
+ };
108201
+ }
108202
+ function buildLabelPromptViewModel(model, selection) {
108203
+ const card = model.columns[selection.columnIndex]?.cards[selection.cardIndex];
108204
+ if (!card)
108205
+ return;
108206
+ const configuredLabels = model.labels ?? [];
108207
+ const draft = new Set(selection.labelDraftIds ?? card.labels);
108208
+ const known = new Set(configuredLabels.map((label) => label.id));
108209
+ return {
108210
+ title: `Edit Labels for ${card.id}`,
108211
+ focused: Boolean(selection.labelOpen),
108212
+ labels: configuredLabels.map((label, index2) => ({
108213
+ id: label.id,
108214
+ title: label.title,
108215
+ checked: draft.has(label.id),
108216
+ focused: index2 === (selection.labelFocusIndex ?? 0)
108217
+ })),
108218
+ unknownLabels: card.labels.filter((label) => !known.has(label)),
108219
+ ...configuredLabels.length === 0 ? {
108220
+ emptyMessage: "No Labels configured. Add Labels in .mikan/config.yaml."
108221
+ } : {},
108222
+ hint: configuredLabels.length === 0 ? "esc close" : "space toggle enter save esc cancel"
108141
108223
  };
108142
108224
  }
108143
108225
  function buildArchivePromptViewModel(model, selection) {
@@ -108166,66 +108248,174 @@ Local Markdown remains the source of truth.`,
108166
108248
  };
108167
108249
  }
108168
108250
 
108251
+ // ../tui/src/prompt-text.ts
108252
+ function renderMoveInteraction(model, selection) {
108253
+ const view = buildMovePromptViewModel(model, selection);
108254
+ if (!view)
108255
+ return ["Move", "No Issue selected"];
108256
+ return [
108257
+ `${view.title} to Status`,
108258
+ ...view.targets.map((target) => `${target.selected ? ">" : " "} ${target.id} (${target.title})`),
108259
+ view.hint
108260
+ ];
108261
+ }
108262
+ function renderLabelInteraction(model, selection) {
108263
+ const view = buildLabelPromptViewModel(model, selection);
108264
+ if (!view)
108265
+ return ["Edit Labels", "No Issue selected"];
108266
+ if (view.emptyMessage) {
108267
+ return [view.title, "", view.emptyMessage, "", view.hint];
108268
+ }
108269
+ return [
108270
+ view.title,
108271
+ "",
108272
+ ...view.labels.map((label) => `${label.focused ? "\u25B6" : " "} [${label.checked ? "x" : " "}] ${label.title}`),
108273
+ ...view.unknownLabels.length > 0 ? ["", `Unknown Labels (read-only): ${view.unknownLabels.join(", ")}`] : [],
108274
+ "",
108275
+ view.hint
108276
+ ];
108277
+ }
108278
+ function renderArchiveInteraction(model, selection) {
108279
+ const view = buildArchivePromptViewModel(model, selection);
108280
+ if (!view)
108281
+ return ["Archive", "No Issue selected"];
108282
+ return [view.title, view.body, view.hint];
108283
+ }
108284
+ function renderGitHubMirrorInteraction(model, selection) {
108285
+ const view = buildGitHubMirrorPromptViewModel(model, selection);
108286
+ if (!view)
108287
+ return ["GitHub Mirror", "No Issue selected"];
108288
+ return [view.title, view.body, view.hint];
108289
+ }
108290
+ function renderWarningDetails(model) {
108291
+ return [
108292
+ "Warning details",
108293
+ ...model.warnings.length > 0 ? model.warnings.map((warning) => `! ${warning}`) : ["No warnings"]
108294
+ ];
108295
+ }
108296
+ function renderKeyHelp() {
108297
+ return [
108298
+ "Key help",
108299
+ "\u2191/\u2193 or j/k card/scroll",
108300
+ "\u2190/\u2192 or h/l column",
108301
+ "enter detail/confirm",
108302
+ "esc back/cancel",
108303
+ "H/L move Issue",
108304
+ "m move menu",
108305
+ "n append Note",
108306
+ "note: enter newline, ctrl+s save",
108307
+ "e edit Labels",
108308
+ "a archive Issue",
108309
+ "g GitHub Mirror",
108310
+ "w warning details",
108311
+ "r reload",
108312
+ "q quit"
108313
+ ];
108314
+ }
108315
+
108169
108316
  // ../tui/src/modals.ts
108170
108317
  function MovePrompt(props) {
108171
- const theme = props.theme ?? buildTuiTheme();
108172
- return import_react3.default.createElement("box", {
108173
- id: "move-modal-backdrop",
108174
- style: modalBackdropStyle(theme)
108175
- }, import_react3.default.createElement("box", {
108176
- id: "move-prompt",
108318
+ return renderModalText({
108319
+ theme: props.theme,
108320
+ backdropId: "move-modal-backdrop",
108321
+ panelId: "move-prompt",
108177
108322
  title: "Move Issue",
108178
- border: true,
108179
- style: modalStyle(theme)
108180
- }, import_react3.default.createElement("text", {
108181
- content: renderMoveInteraction(props.model, props.selection).join(`
108182
- `)
108183
- })));
108323
+ content: renderMoveInteraction(props.model, props.selection)
108324
+ });
108184
108325
  }
108185
108326
  function NotePrompt(props) {
108186
- const theme = props.theme ?? buildTuiTheme();
108187
- return import_react3.default.createElement("box", {
108188
- id: "note-modal-backdrop",
108189
- style: modalBackdropStyle(theme)
108190
- }, import_react3.default.createElement("box", {
108191
- id: "note-prompt",
108327
+ const view = buildNotePromptViewModel(props.model, props.selection);
108328
+ if (!view) {
108329
+ return renderModalText({
108330
+ theme: props.theme,
108331
+ backdropId: "note-modal-backdrop",
108332
+ panelId: "note-prompt",
108333
+ title: "Append Note",
108334
+ content: ["Append note", "No Issue selected"]
108335
+ });
108336
+ }
108337
+ const initialValue = props.selection.noteDraft ?? "";
108338
+ return renderModalShell({
108339
+ theme: props.theme,
108340
+ backdropId: "note-modal-backdrop",
108341
+ panelId: "note-prompt",
108192
108342
  title: "Append Note",
108193
- border: true,
108194
- style: modalStyle(theme)
108343
+ panelStyle: { height: view.feedback ? 14 : 13 }
108195
108344
  }, import_react3.default.createElement("text", {
108196
- content: renderNoteInteraction(props.model, props.selection).join(`
108345
+ content: [view.title, "", "Note:"].join(`
108197
108346
  `)
108198
- })));
108347
+ }), import_react3.default.createElement("textarea", {
108348
+ id: "note-textarea",
108349
+ ref: props.noteTextareaRef,
108350
+ focused: true,
108351
+ initialValue,
108352
+ placeholder: "Write a Note...",
108353
+ height: 5,
108354
+ wrapMode: "word",
108355
+ keyBindings: [{ name: "s", ctrl: true, action: "submit" }],
108356
+ onSubmit: () => {
108357
+ props.onNoteSubmit?.(props.noteTextareaRef?.current?.plainText ?? "");
108358
+ },
108359
+ style: {
108360
+ alignSelf: "stretch",
108361
+ backgroundColor: props.theme?.base.canvas,
108362
+ textColor: props.theme?.base.text,
108363
+ focusedBackgroundColor: props.theme?.base.canvas,
108364
+ focusedTextColor: props.theme?.base.text,
108365
+ width: "auto"
108366
+ }
108367
+ }), import_react3.default.createElement("text", {
108368
+ content: ["", ...view.feedback ? [view.feedback] : [], view.hint].join(`
108369
+ `)
108370
+ }));
108371
+ }
108372
+ function LabelPrompt(props) {
108373
+ return renderModalText({
108374
+ theme: props.theme,
108375
+ backdropId: "label-modal-backdrop",
108376
+ panelId: "label-prompt",
108377
+ title: "Edit Labels",
108378
+ content: renderLabelInteraction(props.model, props.selection)
108379
+ });
108199
108380
  }
108200
108381
  function ArchivePrompt(props) {
108201
- const theme = props.theme ?? buildTuiTheme();
108202
- return import_react3.default.createElement("box", {
108203
- id: "archive-modal-backdrop",
108204
- style: modalBackdropStyle(theme)
108205
- }, import_react3.default.createElement("box", {
108206
- id: "archive-prompt",
108382
+ return renderModalText({
108383
+ theme: props.theme,
108384
+ backdropId: "archive-modal-backdrop",
108385
+ panelId: "archive-prompt",
108207
108386
  title: "Archive Issue",
108208
- border: true,
108209
- style: modalStyle(theme)
108210
- }, import_react3.default.createElement("text", {
108211
- content: renderArchiveInteraction(props.model, props.selection).join(`
108212
- `)
108213
- })));
108387
+ content: renderArchiveInteraction(props.model, props.selection)
108388
+ });
108214
108389
  }
108215
108390
  function GitHubMirrorPrompt(props) {
108216
- const theme = props.theme ?? buildTuiTheme();
108391
+ return renderModalText({
108392
+ theme: props.theme,
108393
+ backdropId: "github-mirror-modal-backdrop",
108394
+ panelId: "github-mirror-prompt",
108395
+ title: "GitHub Mirror",
108396
+ content: renderGitHubMirrorInteraction(props.model, props.selection)
108397
+ });
108398
+ }
108399
+ function renderModalText(options) {
108400
+ return renderModalShell(options, import_react3.default.createElement("text", {
108401
+ content: options.content.join(`
108402
+ `)
108403
+ }));
108404
+ }
108405
+ function renderModalShell(options, ...children) {
108406
+ const theme = options.theme ?? buildTuiTheme();
108217
108407
  return import_react3.default.createElement("box", {
108218
- id: "github-mirror-modal-backdrop",
108408
+ id: options.backdropId,
108219
108409
  style: modalBackdropStyle(theme)
108220
108410
  }, import_react3.default.createElement("box", {
108221
- id: "github-mirror-prompt",
108222
- title: "GitHub Mirror",
108411
+ id: options.panelId,
108412
+ title: options.title,
108223
108413
  border: true,
108224
- style: modalStyle(theme)
108225
- }, import_react3.default.createElement("text", {
108226
- content: renderGitHubMirrorInteraction(props.model, props.selection).join(`
108227
- `)
108228
- })));
108414
+ style: {
108415
+ ...modalStyle(theme),
108416
+ ...options.panelStyle ?? {}
108417
+ }
108418
+ }, ...children));
108229
108419
  }
108230
108420
  function modalBackdropStyle(_theme) {
108231
108421
  return {
@@ -108250,83 +108440,24 @@ function modalStyle(theme) {
108250
108440
  };
108251
108441
  }
108252
108442
  function HelpPanel(props) {
108253
- const theme = props.theme ?? buildTuiTheme();
108254
- return import_react3.default.createElement("box", {
108255
- id: "help-panel-backdrop",
108256
- style: modalBackdropStyle(theme)
108257
- }, import_react3.default.createElement("box", {
108258
- id: "help-panel",
108443
+ return renderModalText({
108444
+ theme: props.theme,
108445
+ backdropId: "help-panel-backdrop",
108446
+ panelId: "help-panel",
108259
108447
  title: "Key help",
108260
- border: true,
108261
- style: modalStyle(theme)
108262
- }, import_react3.default.createElement("text", { content: renderKeyHelp().join(`
108263
- `) })));
108448
+ content: renderKeyHelp()
108449
+ });
108264
108450
  }
108265
108451
  function WarningPanel(props) {
108266
108452
  const theme = props.theme ?? buildTuiTheme();
108267
- return import_react3.default.createElement("box", {
108268
- id: "warning-panel",
108453
+ return renderModalText({
108454
+ theme,
108455
+ backdropId: "warning-panel-backdrop",
108456
+ panelId: "warning-panel",
108269
108457
  title: "Warning details",
108270
- border: true,
108271
- style: {
108272
- backgroundColor: theme.base.surface,
108273
- borderColor: theme.feedback.warning,
108274
- flexDirection: "column"
108275
- }
108276
- }, import_react3.default.createElement("text", {
108277
- content: props.model.warnings.length > 0 ? props.model.warnings.map((warning) => `! ${warning}`).join(`
108278
- `) : "No warnings"
108279
- }));
108280
- }
108281
- function renderMoveInteraction(model, selection) {
108282
- const view = buildMovePromptViewModel(model, selection);
108283
- if (!view)
108284
- return ["Move", "No Issue selected"];
108285
- return [
108286
- `${view.title} to Status`,
108287
- ...view.targets.map((target) => `${target.selected ? ">" : " "} ${target.id} (${target.title})`),
108288
- view.hint
108289
- ];
108290
- }
108291
- function renderNoteInteraction(model, selection) {
108292
- const view = buildNotePromptViewModel(model, selection);
108293
- if (!view)
108294
- return ["Append note", "No Issue selected"];
108295
- return [
108296
- view.title,
108297
- `Note: ${view.draft}`,
108298
- ...view.feedback ? [view.feedback] : [],
108299
- view.hint
108300
- ];
108301
- }
108302
- function renderArchiveInteraction(model, selection) {
108303
- const view = buildArchivePromptViewModel(model, selection);
108304
- if (!view)
108305
- return ["Archive", "No Issue selected"];
108306
- return [view.title, view.body, view.hint];
108307
- }
108308
- function renderGitHubMirrorInteraction(model, selection) {
108309
- const view = buildGitHubMirrorPromptViewModel(model, selection);
108310
- if (!view)
108311
- return ["GitHub Mirror", "No Issue selected"];
108312
- return [view.title, view.body, view.hint];
108313
- }
108314
- function renderKeyHelp() {
108315
- return [
108316
- "Key help",
108317
- "\u2191/\u2193 or j/k card/scroll",
108318
- "\u2190/\u2192 or h/l column",
108319
- "enter detail/confirm",
108320
- "esc back/cancel",
108321
- "H/L move Issue",
108322
- "m move menu",
108323
- "n append Note",
108324
- "a archive Issue",
108325
- "g GitHub Mirror",
108326
- "w warning details",
108327
- "r reload",
108328
- "q quit"
108329
- ];
108458
+ content: renderWarningDetails(props.model).slice(1),
108459
+ panelStyle: { borderColor: theme.feedback.warning }
108460
+ });
108330
108461
  }
108331
108462
 
108332
108463
  // ../tui/src/mutations.ts
@@ -108347,6 +108478,9 @@ function refreshTuiModel(options) {
108347
108478
  moveTargetIndex: stillSelected ? options.selection.moveTargetIndex : undefined,
108348
108479
  noteOpen: stillSelected ? options.selection.noteOpen : false,
108349
108480
  noteDraft: stillSelected ? options.selection.noteDraft : undefined,
108481
+ labelOpen: stillSelected ? options.selection.labelOpen : false,
108482
+ labelFocusIndex: stillSelected ? options.selection.labelFocusIndex : undefined,
108483
+ labelDraftIds: stillSelected ? options.selection.labelDraftIds : undefined,
108350
108484
  message: options.selection.message,
108351
108485
  archiveOpen: stillSelected ? options.selection.archiveOpen : false,
108352
108486
  githubConfirmOpen: stillSelected ? options.selection.githubConfirmOpen : false,
@@ -108439,6 +108573,59 @@ function archiveSelectedIssue(options) {
108439
108573
  selection: { ...result.selection, archiveOpen: false }
108440
108574
  } : result;
108441
108575
  }
108576
+ function updateSelectedIssueLabels(options) {
108577
+ const card = selectedCard(options.model, options.selection);
108578
+ if (!card) {
108579
+ return {
108580
+ ok: false,
108581
+ model: options.model,
108582
+ selection: { ...options.selection, labelOpen: false },
108583
+ message: "No Issue selected"
108584
+ };
108585
+ }
108586
+ const loaded = loadProjectConfig(options.cwd ?? process.cwd());
108587
+ if (!loaded.ok) {
108588
+ return {
108589
+ ok: false,
108590
+ model: options.model,
108591
+ selection: { ...options.selection, labelOpen: false },
108592
+ message: loaded.error.message
108593
+ };
108594
+ }
108595
+ const selectedKnown = new Set(options.selection.labelDraftIds ?? []);
108596
+ const configuredIds = loaded.value.config.labels.map((label) => label.id);
108597
+ const configuredSet = new Set(configuredIds);
108598
+ const knownLabels = configuredIds.filter((label) => selectedKnown.has(label));
108599
+ const unknownLabels = card.labels.filter((label) => !configuredSet.has(label));
108600
+ const updated = updateIssue({
108601
+ projectRoot: loaded.value.projectRoot,
108602
+ config: loaded.value.config,
108603
+ id: card.id,
108604
+ labels: [...knownLabels, ...unknownLabels],
108605
+ preserveUnknownLabels: true,
108606
+ now: options.now
108607
+ });
108608
+ if (!updated.ok) {
108609
+ return {
108610
+ ok: false,
108611
+ model: options.model,
108612
+ selection: { ...options.selection, labelOpen: false },
108613
+ message: updated.error.message
108614
+ };
108615
+ }
108616
+ const model = loadTuiModel(options.cwd);
108617
+ const selection = findSelectionByCardId(model, card.id) ?? clampSelection(model, options.selection);
108618
+ return {
108619
+ ok: true,
108620
+ model,
108621
+ selection: {
108622
+ ...selection,
108623
+ detailOpen: options.selection.detailOpen,
108624
+ labelOpen: false
108625
+ },
108626
+ message: `${card.id} Labels updated`
108627
+ };
108628
+ }
108442
108629
  async function beginSelectedIssueGitHubMirror(options) {
108443
108630
  if (options.selection.githubBusy)
108444
108631
  return githubAlreadyRunning(options);
@@ -108608,7 +108795,7 @@ function appendSelectedIssueNote(options) {
108608
108795
  return {
108609
108796
  ok: false,
108610
108797
  model: options.model,
108611
- selection: { ...options.selection, noteOpen: false },
108798
+ selection: { ...options.selection, noteOpen: true },
108612
108799
  message: "Note cannot be empty"
108613
108800
  };
108614
108801
  }
@@ -108655,7 +108842,8 @@ function appendSelectedIssueNote(options) {
108655
108842
  selection: {
108656
108843
  ...selection,
108657
108844
  detailOpen: options.selection.detailOpen,
108658
- noteOpen: false
108845
+ noteOpen: false,
108846
+ noteDraft: undefined
108659
108847
  },
108660
108848
  message: `${card.id} note appended`
108661
108849
  };
@@ -108672,7 +108860,9 @@ function TuiAppView({
108672
108860
  theme = buildTuiTheme(),
108673
108861
  viewportHeight,
108674
108862
  viewportWidth,
108675
- columns
108863
+ columns,
108864
+ noteTextareaRef,
108865
+ onNoteSubmit
108676
108866
  }) {
108677
108867
  const details = selection.detailOpen ? getSelectedDetails(model, selection) : undefined;
108678
108868
  return import_react20.default.createElement("box", {
@@ -108698,7 +108888,13 @@ function TuiAppView({
108698
108888
  viewportHeight,
108699
108889
  viewportWidth,
108700
108890
  columns
108701
- })), selection.moveOpen ? import_react20.default.createElement(MovePrompt, { model, selection, theme }) : undefined, selection.noteOpen ? import_react20.default.createElement(NotePrompt, { model, selection, theme }) : undefined, selection.archiveOpen ? import_react20.default.createElement(ArchivePrompt, { model, selection, theme }) : undefined, selection.githubConfirmOpen ? import_react20.default.createElement(GitHubMirrorPrompt, { model, selection, theme }) : undefined, selection.warningsOpen ? import_react20.default.createElement(WarningPanel, { model, theme }) : undefined, selection.helpOpen ? import_react20.default.createElement(HelpPanel, { theme }) : undefined, import_react20.default.createElement(Footer, {
108891
+ })), selection.moveOpen ? import_react20.default.createElement(MovePrompt, { model, selection, theme }) : undefined, selection.noteOpen ? import_react20.default.createElement(NotePrompt, {
108892
+ model,
108893
+ selection,
108894
+ theme,
108895
+ noteTextareaRef,
108896
+ onNoteSubmit
108897
+ }) : undefined, selection.labelOpen ? import_react20.default.createElement(LabelPrompt, { model, selection, theme }) : undefined, selection.archiveOpen ? import_react20.default.createElement(ArchivePrompt, { model, selection, theme }) : undefined, selection.githubConfirmOpen ? import_react20.default.createElement(GitHubMirrorPrompt, { model, selection, theme }) : undefined, selection.warningsOpen ? import_react20.default.createElement(WarningPanel, { model, theme }) : undefined, selection.helpOpen ? import_react20.default.createElement(HelpPanel, { theme }) : undefined, import_react20.default.createElement(Footer, {
108702
108898
  message: selection.message,
108703
108899
  mode: footerMode(selection),
108704
108900
  theme
@@ -108732,6 +108928,19 @@ async function launchTui(options = {}) {
108732
108928
  const modelRef = import_react20.default.useRef(model);
108733
108929
  const selectionRef = import_react20.default.useRef(selection);
108734
108930
  const githubBusyRef = import_react20.default.useRef(false);
108931
+ const noteTextareaRef = import_react20.default.useRef(null);
108932
+ const submitNote = import_react20.default.useCallback((body) => {
108933
+ const result = appendSelectedIssueNote({
108934
+ cwd: options.cwd,
108935
+ model: modelRef.current,
108936
+ selection: selectionRef.current,
108937
+ body
108938
+ });
108939
+ modelRef.current = result.model;
108940
+ selectionRef.current = { ...result.selection, message: result.message };
108941
+ setModel(result.model);
108942
+ setSelection({ ...result.selection, message: result.message });
108943
+ }, []);
108735
108944
  modelRef.current = model;
108736
108945
  selectionRef.current = selection;
108737
108946
  import_react20.default.useEffect(() => {
@@ -108749,7 +108958,7 @@ async function launchTui(options = {}) {
108749
108958
  return () => clearInterval(interval);
108750
108959
  }, []);
108751
108960
  useKeyboard2((key) => {
108752
- const action = keyToTuiAction(key.name, key.shift);
108961
+ const action = keyToTuiAction(key.name, key.shift, key.ctrl);
108753
108962
  if (selection.helpOpen) {
108754
108963
  if (action === "escape" || action === "help") {
108755
108964
  setSelection((current) => moveSelection(model, current, action));
@@ -108757,26 +108966,10 @@ async function launchTui(options = {}) {
108757
108966
  return;
108758
108967
  }
108759
108968
  if (selection.noteOpen) {
108760
- if (action === "help") {
108761
- setSelection((current) => moveSelection(model, current, action));
108762
- return;
108763
- }
108764
108969
  if (action === "escape") {
108765
108970
  setSelection((current) => moveSelection(model, current, action));
108766
108971
  return;
108767
108972
  }
108768
- if (action === "enter") {
108769
- const result = appendSelectedIssueNote({
108770
- cwd: options.cwd,
108771
- model,
108772
- selection,
108773
- body: selection.noteDraft ?? ""
108774
- });
108775
- setModel(result.model);
108776
- setSelection({ ...result.selection, message: result.message });
108777
- return;
108778
- }
108779
- setSelection((current) => applyNoteInput(current, key.name, key.shift));
108780
108973
  return;
108781
108974
  }
108782
108975
  if (selection.archiveOpen) {
@@ -108831,6 +109024,35 @@ async function launchTui(options = {}) {
108831
109024
  }
108832
109025
  return;
108833
109026
  }
109027
+ if (selection.labelOpen) {
109028
+ if (action === "help") {
109029
+ setSelection((current) => moveSelection(model, current, action));
109030
+ return;
109031
+ }
109032
+ if (action === "escape") {
109033
+ setSelection((current) => moveSelection(model, current, action));
109034
+ return;
109035
+ }
109036
+ if (action === "up" || action === "down") {
109037
+ setSelection((current) => moveLabelFocus(model, current, action));
109038
+ return;
109039
+ }
109040
+ if (key.name === "space") {
109041
+ setSelection((current) => toggleFocusedLabel(model, current));
109042
+ return;
109043
+ }
109044
+ if (action === "enter") {
109045
+ const result = updateSelectedIssueLabels({
109046
+ cwd: options.cwd,
109047
+ model,
109048
+ selection
109049
+ });
109050
+ setModel(result.model);
109051
+ setSelection({ ...result.selection, message: result.message });
109052
+ return;
109053
+ }
109054
+ return;
109055
+ }
108834
109056
  if (!action)
108835
109057
  return;
108836
109058
  if (action === "quit") {
@@ -108880,6 +109102,8 @@ async function launchTui(options = {}) {
108880
109102
  setSelection((current) => moveSelection(model, current, action));
108881
109103
  return;
108882
109104
  }
109105
+ if (action === "save-note")
109106
+ return;
108883
109107
  if (action === "github") {
108884
109108
  if (githubBusyRef.current)
108885
109109
  return;
@@ -108912,7 +109136,9 @@ async function launchTui(options = {}) {
108912
109136
  selection,
108913
109137
  viewportHeight: renderer.height,
108914
109138
  viewportWidth: renderer.width,
108915
- columns: options.columns
109139
+ columns: options.columns,
109140
+ noteTextareaRef,
109141
+ onNoteSubmit: submitNote
108916
109142
  });
108917
109143
  }
108918
109144
  root.render(import_react20.default.createElement(App));
@@ -109191,7 +109417,10 @@ function fireHooks(loaded, issue2, fromStatus, toStatus, options) {
109191
109417
  ...hooks?.on_enter?.[toStatus] ?? [],
109192
109418
  ...hooks?.on_transition?.[`${fromStatus}->${toStatus}`] ?? []
109193
109419
  ];
109194
- for (const command of commands2) {
109420
+ for (const entry of commands2) {
109421
+ const command = hookCommandForIssue(entry, loaded, issue2, options);
109422
+ if (!command)
109423
+ continue;
109195
109424
  const rendered = renderHookCommand(command, {
109196
109425
  project_root: loaded.projectRoot,
109197
109426
  issue_path: issue2.path,
@@ -109217,6 +109446,22 @@ function fireHooks(loaded, issue2, fromStatus, toStatus, options) {
109217
109446
  }
109218
109447
  }
109219
109448
  }
109449
+ function hookCommandForIssue(entry, loaded, issue2, options) {
109450
+ if (typeof entry === "string")
109451
+ return entry;
109452
+ const labelsInclude = entry.when?.labels_include;
109453
+ if (!labelsInclude)
109454
+ return entry.command;
109455
+ const knownLabels = new Set(loaded.config.labels.map((label) => label.id));
109456
+ for (const label of labelsInclude) {
109457
+ if (!knownLabels.has(label)) {
109458
+ emitError(options, `hook skipped: ${String(issue2.issue.id)} references unknown label in hook filter: ${label}`);
109459
+ return;
109460
+ }
109461
+ }
109462
+ const issueLabels = new Set(issue2.issue.labels.map(String));
109463
+ return labelsInclude.every((label) => issueLabels.has(label)) ? entry.command : undefined;
109464
+ }
109220
109465
  function recordGitHubMirrorPushFailure(projectRoot, issueId, message, options) {
109221
109466
  appendHookFailure(projectRoot, {
109222
109467
  timestamp: utcNow3(options.now),
@@ -109628,6 +109873,10 @@ function helpText() {
109628
109873
  Usage:
109629
109874
  mikan <command> [options]
109630
109875
 
109876
+ Options:
109877
+ -v, --version Print mikan version
109878
+ -h, --help Show this help
109879
+
109631
109880
  Commands:
109632
109881
  init Create .mikan project files
109633
109882
  add Create an Issue
@@ -109858,6 +110107,9 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
109858
110107
  const command = argv[0];
109859
110108
  if (!command || isHelpFlag(command))
109860
110109
  return ok2(helpText());
110110
+ if (isVersionFlag(command))
110111
+ return ok2(`${package_default.version}
110112
+ `);
109861
110113
  if (command === "help") {
109862
110114
  const topic = argv[1];
109863
110115
  return topic ? commandHelp(topic) : ok2(helpText());
@@ -109916,6 +110168,9 @@ Run \`mikan help tui\` for usage.`);
109916
110168
  return fail2(error51 instanceof Error ? error51.message : String(error51));
109917
110169
  }
109918
110170
  }
110171
+ function isVersionFlag(input) {
110172
+ return input === "-v" || input === "--version";
110173
+ }
109919
110174
  async function main(argv = process.argv.slice(2)) {
109920
110175
  const result = await runInteractiveCommand(argv, { cwd: process.cwd() });
109921
110176
  if (result.stdout)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takemo101/mikan",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {