kanban-lite 1.2.2 → 1.2.4

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 (39) hide show
  1. package/dist/cli.js +257 -90
  2. package/dist/extension.js +245 -78
  3. package/dist/mcp-server.js +117 -33
  4. package/dist/sdk/index.cjs +117 -32
  5. package/dist/sdk/index.mjs +117 -32
  6. package/dist/sdk/sdk/KanbanSDK.d.ts +27 -10
  7. package/dist/sdk/sdk/modules/cards.d.ts +11 -2
  8. package/dist/sdk/sdk/plugins/index.d.ts +7 -0
  9. package/dist/sdk/sdk/types.d.ts +12 -27
  10. package/dist/sdk/shared/config.d.ts +17 -1
  11. package/dist/standalone-webview/index.js +38 -38
  12. package/dist/standalone-webview/index.js.map +1 -1
  13. package/dist/standalone.js +307 -125
  14. package/package.json +1 -1
  15. package/src/cli/index.test.ts +157 -0
  16. package/src/cli/index.ts +1 -1
  17. package/src/mcp-server/index.test.ts +76 -0
  18. package/src/mcp-server/index.ts +1 -1
  19. package/src/sdk/KanbanSDK.d.ts +26 -10
  20. package/src/sdk/KanbanSDK.ts +37 -11
  21. package/src/sdk/__tests__/KanbanSDK.test.ts +79 -24
  22. package/src/sdk/integrationCatalog.ts +1 -0
  23. package/src/sdk/modules/cards.ts +13 -24
  24. package/src/sdk/plugins/index.d.ts +7 -0
  25. package/src/sdk/plugins/index.ts +17 -2
  26. package/src/sdk/types.d.ts +10 -26
  27. package/src/sdk/types.ts +11 -24
  28. package/src/sdk/webhooks.ts +19 -2
  29. package/src/shared/config.ts +130 -2
  30. package/src/standalone/__tests__/server.integration.test.ts +81 -2
  31. package/src/standalone/internal/runtime.ts +11 -6
  32. package/src/standalone/internal/websocket.ts +13 -3
  33. package/src/standalone/server.ts +67 -9
  34. package/src/standalone/watcherSetup.ts +9 -0
  35. package/src/webview/standalone-shim.ts +2 -1
  36. package/tmp/screenshots-workspace/.kanban/.active-card.json +5 -0
  37. package/tmp/screenshots-workspace/.kanban/boards/default/deleted/1-dddd.md +17 -0
  38. package/tmp/screenshots-workspace/.kanban/boards/default/deleted/attachments/1.log +1 -0
  39. package/tmp/screenshots-workspace/.kanban.json +59 -0
package/dist/cli.js CHANGED
@@ -173,11 +173,87 @@ function migrateConfigV1ToV2(raw) {
173
173
  }
174
174
  return v2;
175
175
  }
176
+ function loadDotEnv(dir) {
177
+ const envPath = path.join(dir, ".env");
178
+ let content;
179
+ try {
180
+ content = fs.readFileSync(envPath, "utf-8");
181
+ } catch {
182
+ return;
183
+ }
184
+ for (const line of content.split("\n")) {
185
+ const trimmed = line.trim();
186
+ if (!trimmed || trimmed.startsWith("#"))
187
+ continue;
188
+ const eqIdx = trimmed.indexOf("=");
189
+ if (eqIdx < 1)
190
+ continue;
191
+ const key = trimmed.slice(0, eqIdx).trim();
192
+ let val = trimmed.slice(eqIdx + 1).trim();
193
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
194
+ val = val.slice(1, -1);
195
+ }
196
+ if (process.env[key] === void 0) {
197
+ process.env[key] = val;
198
+ }
199
+ }
200
+ }
201
+ function resolveConfigEnvVars(node, configFileName, nodePath = "") {
202
+ const isFormDefaultDataPath = /^\.forms\.(?:[^.]+|"[^"]+")\.data(?:$|[.\[])/.test(nodePath);
203
+ if (isFormDefaultDataPath) {
204
+ return node;
205
+ }
206
+ if (typeof node === "string") {
207
+ return node.replace(/\$\{([^}]+)\}/g, (_match, varName) => {
208
+ const envValue = process.env[varName];
209
+ if (envValue === void 0) {
210
+ throw new Error(
211
+ `missing ${varName} in ${configFileName}: ${nodePath} "${node}"`
212
+ );
213
+ }
214
+ return envValue;
215
+ });
216
+ }
217
+ if (Array.isArray(node)) {
218
+ for (let i = 0; i < node.length; i++) {
219
+ node[i] = resolveConfigEnvVars(node[i], configFileName, `${nodePath}[${i}]`);
220
+ }
221
+ return node;
222
+ }
223
+ if (node !== null && typeof node === "object") {
224
+ const obj = node;
225
+ for (const key of Object.keys(obj)) {
226
+ const jsonKey = /[^a-zA-Z0-9_]/.test(key) ? `"${key}"` : key;
227
+ const childPath = nodePath ? `${nodePath}.${jsonKey}` : `.${jsonKey}`;
228
+ obj[key] = resolveConfigEnvVars(obj[key], configFileName, childPath);
229
+ }
230
+ return obj;
231
+ }
232
+ return node;
233
+ }
176
234
  function readConfig(workspaceRoot) {
177
235
  const filePath = configPath(workspaceRoot);
178
236
  const defaults = { ...DEFAULT_CONFIG, boards: { default: { ...DEFAULT_BOARD_CONFIG, columns: [...DEFAULT_COLUMNS] } } };
237
+ let raw;
238
+ try {
239
+ raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
240
+ } catch {
241
+ return defaults;
242
+ }
243
+ loadDotEnv(workspaceRoot);
244
+ try {
245
+ resolveConfigEnvVars(raw, CONFIG_FILENAME);
246
+ } catch (err) {
247
+ const msg = err instanceof Error ? err.message : String(err);
248
+ process.stderr.write(`
249
+ Configuration error: ${msg}
250
+
251
+ Set the missing environment variable before starting the server.
252
+
253
+ `);
254
+ process.exit(1);
255
+ }
179
256
  try {
180
- const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
181
257
  const isV1 = raw.version === 1 || !raw.version && !(typeof raw.boards === "object" && raw.boards !== null && !Array.isArray(raw.boards));
182
258
  if (isV1) {
183
259
  const v2 = migrateConfigV1ToV2(raw);
@@ -5315,7 +5391,7 @@ function resolveAuthIdentityPlugin(ref) {
5315
5391
  if (ref.provider === "rbac")
5316
5392
  return RBAC_IDENTITY_PLUGIN;
5317
5393
  const packageName = AUTH_PROVIDER_ALIASES.get(ref.provider) ?? ref.provider;
5318
- return loadExternalAuthIdentityPlugin(packageName, ref.provider);
5394
+ return loadExternalAuthIdentityPlugin(packageName, ref.provider, ref.options);
5319
5395
  }
5320
5396
  function resolveAuthPolicyPlugin(ref) {
5321
5397
  if (ref.provider === "noop")
@@ -5447,8 +5523,13 @@ function selectAuthPolicyPlugin(mod, providerId) {
5447
5523
  return direct;
5448
5524
  return null;
5449
5525
  }
5450
- function loadExternalAuthIdentityPlugin(packageName, providerId) {
5526
+ function loadExternalAuthIdentityPlugin(packageName, providerId, options) {
5451
5527
  const mod = loadExternalModule(packageName);
5528
+ if (options !== void 0 && typeof mod.createAuthIdentityPlugin === "function") {
5529
+ const created = mod.createAuthIdentityPlugin(options);
5530
+ if (isValidAuthIdentityPlugin(created, providerId))
5531
+ return created;
5532
+ }
5452
5533
  const plugin = selectAuthIdentityPlugin(mod, providerId);
5453
5534
  if (!plugin) {
5454
5535
  throw new Error(
@@ -24633,29 +24714,10 @@ async function updateCard(ctx, { cardId, updates, boardId }) {
24633
24714
  return card;
24634
24715
  }
24635
24716
  async function triggerAction(ctx, { cardId, action, boardId }) {
24636
- const config3 = readConfig(ctx.workspaceRoot);
24637
- const { actionWebhookUrl } = config3;
24638
- if (!actionWebhookUrl) {
24639
- throw new Error("No action webhook URL configured. Set actionWebhookUrl in .kanban.json");
24640
- }
24641
24717
  const card = await getCard(ctx, { cardId, boardId });
24642
24718
  if (!card)
24643
24719
  throw new Error(`Card not found: ${cardId}`);
24644
24720
  const resolvedBoardId = card.boardId || ctx._resolveBoardId(boardId);
24645
- const payload = {
24646
- action,
24647
- board: resolvedBoardId,
24648
- list: card.status,
24649
- card: sanitizeCard(card)
24650
- };
24651
- const response = await fetch(actionWebhookUrl, {
24652
- method: "POST",
24653
- headers: { "Content-Type": "application/json" },
24654
- body: JSON.stringify(payload)
24655
- });
24656
- if (!response.ok) {
24657
- throw new Error(`Action webhook responded with ${response.status}: ${response.statusText}`);
24658
- }
24659
24721
  await appendActivityLog(ctx, {
24660
24722
  cardId,
24661
24723
  boardId: resolvedBoardId,
@@ -24666,6 +24728,12 @@ async function triggerAction(ctx, { cardId, action, boardId }) {
24666
24728
  }
24667
24729
  }).catch(() => {
24668
24730
  });
24731
+ return {
24732
+ action,
24733
+ board: resolvedBoardId,
24734
+ list: card.status,
24735
+ card: sanitizeCard(card)
24736
+ };
24669
24737
  }
24670
24738
  async function submitForm(ctx, input) {
24671
24739
  const card = await getCard(ctx, { cardId: input.cardId, boardId: input.boardId });
@@ -26170,6 +26238,25 @@ var init_KanbanSDK = __esm({
26170
26238
  get workspaceRoot() {
26171
26239
  return path14.dirname(this.kanbanDir);
26172
26240
  }
26241
+ /**
26242
+ * Returns a cloned read-only snapshot of the current workspace config.
26243
+ *
26244
+ * The returned snapshot is created from a fresh config read and deep-cloned
26245
+ * before being returned, so callers receive an isolated view of the current
26246
+ * `.kanban.json` state rather than a live mutable runtime object. Mutating the
26247
+ * returned snapshot does not update persisted config or affect this SDK instance.
26248
+ *
26249
+ * @returns A cloned read-only snapshot of the current {@link KanbanConfig}.
26250
+ *
26251
+ * @example
26252
+ * ```ts
26253
+ * const config = sdk.getConfigSnapshot()
26254
+ * console.log(config.defaultBoard)
26255
+ * ```
26256
+ */
26257
+ getConfigSnapshot() {
26258
+ return structuredClone(readConfig(this.workspaceRoot));
26259
+ }
26173
26260
  // --- Board resolution helpers ---
26174
26261
  /** @internal */
26175
26262
  _resolveBoardId(boardId) {
@@ -26636,21 +26723,17 @@ var init_KanbanSDK = __esm({
26636
26723
  return result;
26637
26724
  }
26638
26725
  /**
26639
- * Triggers a named action for a card by POSTing to the global `actionWebhookUrl`
26640
- * configured in `.kanban.json`.
26726
+ * Triggers a named action for a card.
26641
26727
  *
26642
- * The payload sent to the webhook is:
26643
- * ```json
26644
- * { "action": "retry", "board": "default", "list": "in-progress", "card": { ...sanitizedCard } }
26645
- * ```
26728
+ * Validates the card, appends an activity log entry, and emits the
26729
+ * `card.action.triggered` after-event so registered webhooks receive
26730
+ * the action payload automatically.
26646
26731
  *
26647
26732
  * @param cardId - The ID of the card to trigger the action for.
26648
26733
  * @param action - The action name string (e.g. `'retry'`, `'sendEmail'`).
26649
26734
  * @param boardId - Optional board ID. Defaults to the workspace's default board.
26650
- * @returns A promise resolving when the webhook responds with 2xx.
26651
- * @throws {Error} If no `actionWebhookUrl` is configured in `.kanban.json`.
26735
+ * @returns A promise that resolves when the action has been processed.
26652
26736
  * @throws {Error} If the card is not found.
26653
- * @throws {Error} If the webhook responds with a non-2xx status.
26654
26737
  *
26655
26738
  * @example
26656
26739
  * ```ts
@@ -26660,7 +26743,8 @@ var init_KanbanSDK = __esm({
26660
26743
  */
26661
26744
  async triggerAction(cardId, action, boardId) {
26662
26745
  const mergedInput = await this._runBeforeEvent("card.action.trigger", { cardId, action, boardId }, void 0, boardId);
26663
- return triggerAction(this, mergedInput);
26746
+ const payload = await triggerAction(this, mergedInput);
26747
+ this._runAfterEvent("card.action.triggered", payload, void 0, payload.board);
26664
26748
  }
26665
26749
  /**
26666
26750
  * Moves a card to a different status column and/or position within that column.
@@ -60340,7 +60424,7 @@ async function main() {
60340
60424
  );
60341
60425
  server.tool(
60342
60426
  "trigger_action",
60343
- "Trigger a named action on a card. The action name must match one of the card's configured actions. Calls the configured action webhook URL with the action name and card details.",
60427
+ "Trigger a named action on a card. The action name must match one of the card's configured actions. Emits a card.action.triggered event delivered to registered webhooks.",
60344
60428
  {
60345
60429
  card_id: external_exports4.string().describe("Card ID (partial match supported)"),
60346
60430
  action: external_exports4.string().describe("Action name to trigger"),
@@ -66751,7 +66835,7 @@ var require_thread_stream = __commonJS({
66751
66835
  var { version: version3 } = require_package();
66752
66836
  var { EventEmitter: EventEmitter3 } = require("events");
66753
66837
  var { Worker } = require("worker_threads");
66754
- var { join: join21 } = require("path");
66838
+ var { join: join22 } = require("path");
66755
66839
  var { pathToFileURL } = require("url");
66756
66840
  var { wait } = require_wait();
66757
66841
  var {
@@ -66787,7 +66871,7 @@ var require_thread_stream = __commonJS({
66787
66871
  function createWorker(stream, opts) {
66788
66872
  const { filename, workerData } = opts;
66789
66873
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
66790
- const toExecute = bundlerOverrides["thread-stream-worker"] || join21(__dirname, "lib", "worker.js");
66874
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join22(__dirname, "lib", "worker.js");
66791
66875
  const worker = new Worker(toExecute, {
66792
66876
  ...opts.workerOpts,
66793
66877
  trackUnmanagedFds: false,
@@ -67178,7 +67262,7 @@ var require_transport = __commonJS({
67178
67262
  var { createRequire: createRequire2 } = require("module");
67179
67263
  var { existsSync: existsSync4 } = require("node:fs");
67180
67264
  var getCallers = require_caller();
67181
- var { join: join21, isAbsolute: isAbsolute2, sep: sep2 } = require("node:path");
67265
+ var { join: join22, isAbsolute: isAbsolute2, sep: sep2 } = require("node:path");
67182
67266
  var { fileURLToPath: fileURLToPath2 } = require("node:url");
67183
67267
  var sleep = require_atomic_sleep();
67184
67268
  var onExit = require_on_exit_leak_free();
@@ -67331,7 +67415,7 @@ var require_transport = __commonJS({
67331
67415
  throw new Error("only one of target or targets can be specified");
67332
67416
  }
67333
67417
  if (targets) {
67334
- target = bundlerOverrides["pino-worker"] || join21(__dirname, "worker.js");
67418
+ target = bundlerOverrides["pino-worker"] || join22(__dirname, "worker.js");
67335
67419
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
67336
67420
  return {
67337
67421
  ...dest,
@@ -67349,7 +67433,7 @@ var require_transport = __commonJS({
67349
67433
  });
67350
67434
  });
67351
67435
  } else if (pipeline) {
67352
- target = bundlerOverrides["pino-worker"] || join21(__dirname, "worker.js");
67436
+ target = bundlerOverrides["pino-worker"] || join22(__dirname, "worker.js");
67353
67437
  options.pipelines = [pipeline.map((dest) => {
67354
67438
  return {
67355
67439
  ...dest,
@@ -67372,7 +67456,7 @@ var require_transport = __commonJS({
67372
67456
  return origin;
67373
67457
  }
67374
67458
  if (origin === "pino/file") {
67375
- return join21(__dirname, "..", "file.js");
67459
+ return join22(__dirname, "..", "file.js");
67376
67460
  }
67377
67461
  let fixTarget2;
67378
67462
  for (const filePath of callers) {
@@ -68362,7 +68446,7 @@ var require_safe_stable_stringify = __commonJS({
68362
68446
  return circularValue;
68363
68447
  }
68364
68448
  let res = "";
68365
- let join21 = ",";
68449
+ let join22 = ",";
68366
68450
  const originalIndentation = indentation;
68367
68451
  if (Array.isArray(value)) {
68368
68452
  if (value.length === 0) {
@@ -68376,7 +68460,7 @@ var require_safe_stable_stringify = __commonJS({
68376
68460
  indentation += spacer;
68377
68461
  res += `
68378
68462
  ${indentation}`;
68379
- join21 = `,
68463
+ join22 = `,
68380
68464
  ${indentation}`;
68381
68465
  }
68382
68466
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -68384,13 +68468,13 @@ ${indentation}`;
68384
68468
  for (; i < maximumValuesToStringify - 1; i++) {
68385
68469
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
68386
68470
  res += tmp2 !== void 0 ? tmp2 : "null";
68387
- res += join21;
68471
+ res += join22;
68388
68472
  }
68389
68473
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
68390
68474
  res += tmp !== void 0 ? tmp : "null";
68391
68475
  if (value.length - 1 > maximumBreadth) {
68392
68476
  const removedKeys = value.length - maximumBreadth - 1;
68393
- res += `${join21}"... ${getItemCount(removedKeys)} not stringified"`;
68477
+ res += `${join22}"... ${getItemCount(removedKeys)} not stringified"`;
68394
68478
  }
68395
68479
  if (spacer !== "") {
68396
68480
  res += `
@@ -68411,7 +68495,7 @@ ${originalIndentation}`;
68411
68495
  let separator = "";
68412
68496
  if (spacer !== "") {
68413
68497
  indentation += spacer;
68414
- join21 = `,
68498
+ join22 = `,
68415
68499
  ${indentation}`;
68416
68500
  whitespace = " ";
68417
68501
  }
@@ -68425,13 +68509,13 @@ ${indentation}`;
68425
68509
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
68426
68510
  if (tmp !== void 0) {
68427
68511
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
68428
- separator = join21;
68512
+ separator = join22;
68429
68513
  }
68430
68514
  }
68431
68515
  if (keyLength > maximumBreadth) {
68432
68516
  const removedKeys = keyLength - maximumBreadth;
68433
68517
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
68434
- separator = join21;
68518
+ separator = join22;
68435
68519
  }
68436
68520
  if (spacer !== "" && separator.length > 1) {
68437
68521
  res = `
@@ -68471,7 +68555,7 @@ ${originalIndentation}`;
68471
68555
  }
68472
68556
  const originalIndentation = indentation;
68473
68557
  let res = "";
68474
- let join21 = ",";
68558
+ let join22 = ",";
68475
68559
  if (Array.isArray(value)) {
68476
68560
  if (value.length === 0) {
68477
68561
  return "[]";
@@ -68484,7 +68568,7 @@ ${originalIndentation}`;
68484
68568
  indentation += spacer;
68485
68569
  res += `
68486
68570
  ${indentation}`;
68487
- join21 = `,
68571
+ join22 = `,
68488
68572
  ${indentation}`;
68489
68573
  }
68490
68574
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -68492,13 +68576,13 @@ ${indentation}`;
68492
68576
  for (; i < maximumValuesToStringify - 1; i++) {
68493
68577
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
68494
68578
  res += tmp2 !== void 0 ? tmp2 : "null";
68495
- res += join21;
68579
+ res += join22;
68496
68580
  }
68497
68581
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
68498
68582
  res += tmp !== void 0 ? tmp : "null";
68499
68583
  if (value.length - 1 > maximumBreadth) {
68500
68584
  const removedKeys = value.length - maximumBreadth - 1;
68501
- res += `${join21}"... ${getItemCount(removedKeys)} not stringified"`;
68585
+ res += `${join22}"... ${getItemCount(removedKeys)} not stringified"`;
68502
68586
  }
68503
68587
  if (spacer !== "") {
68504
68588
  res += `
@@ -68511,7 +68595,7 @@ ${originalIndentation}`;
68511
68595
  let whitespace = "";
68512
68596
  if (spacer !== "") {
68513
68597
  indentation += spacer;
68514
- join21 = `,
68598
+ join22 = `,
68515
68599
  ${indentation}`;
68516
68600
  whitespace = " ";
68517
68601
  }
@@ -68520,7 +68604,7 @@ ${indentation}`;
68520
68604
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
68521
68605
  if (tmp !== void 0) {
68522
68606
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
68523
- separator = join21;
68607
+ separator = join22;
68524
68608
  }
68525
68609
  }
68526
68610
  if (spacer !== "" && separator.length > 1) {
@@ -68577,20 +68661,20 @@ ${originalIndentation}`;
68577
68661
  indentation += spacer;
68578
68662
  let res2 = `
68579
68663
  ${indentation}`;
68580
- const join22 = `,
68664
+ const join23 = `,
68581
68665
  ${indentation}`;
68582
68666
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
68583
68667
  let i = 0;
68584
68668
  for (; i < maximumValuesToStringify - 1; i++) {
68585
68669
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
68586
68670
  res2 += tmp2 !== void 0 ? tmp2 : "null";
68587
- res2 += join22;
68671
+ res2 += join23;
68588
68672
  }
68589
68673
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
68590
68674
  res2 += tmp !== void 0 ? tmp : "null";
68591
68675
  if (value.length - 1 > maximumBreadth) {
68592
68676
  const removedKeys = value.length - maximumBreadth - 1;
68593
- res2 += `${join22}"... ${getItemCount(removedKeys)} not stringified"`;
68677
+ res2 += `${join23}"... ${getItemCount(removedKeys)} not stringified"`;
68594
68678
  }
68595
68679
  res2 += `
68596
68680
  ${originalIndentation}`;
@@ -68606,16 +68690,16 @@ ${originalIndentation}`;
68606
68690
  return '"[Object]"';
68607
68691
  }
68608
68692
  indentation += spacer;
68609
- const join21 = `,
68693
+ const join22 = `,
68610
68694
  ${indentation}`;
68611
68695
  let res = "";
68612
68696
  let separator = "";
68613
68697
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
68614
68698
  if (isTypedArrayWithEntries(value)) {
68615
- res += stringifyTypedArray(value, join21, maximumBreadth);
68699
+ res += stringifyTypedArray(value, join22, maximumBreadth);
68616
68700
  keys = keys.slice(value.length);
68617
68701
  maximumPropertiesToStringify -= value.length;
68618
- separator = join21;
68702
+ separator = join22;
68619
68703
  }
68620
68704
  if (deterministic) {
68621
68705
  keys = sort(keys, comparator);
@@ -68626,13 +68710,13 @@ ${indentation}`;
68626
68710
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
68627
68711
  if (tmp !== void 0) {
68628
68712
  res += `${separator}${strEscape(key2)}: ${tmp}`;
68629
- separator = join21;
68713
+ separator = join22;
68630
68714
  }
68631
68715
  }
68632
68716
  if (keyLength > maximumBreadth) {
68633
68717
  const removedKeys = keyLength - maximumBreadth;
68634
68718
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
68635
- separator = join21;
68719
+ separator = join22;
68636
68720
  }
68637
68721
  if (separator !== "") {
68638
68722
  res = `
@@ -103100,7 +103184,7 @@ var require_send = __commonJS({
103100
103184
  var { parseTokenList } = require_parseTokenList();
103101
103185
  var { createHttpError } = require_createHttpError();
103102
103186
  var extname4 = path24.extname;
103103
- var join21 = path24.join;
103187
+ var join22 = path24.join;
103104
103188
  var normalize2 = path24.normalize;
103105
103189
  var resolve11 = path24.resolve;
103106
103190
  var sep2 = path24.sep;
@@ -103187,7 +103271,7 @@ var require_send = __commonJS({
103187
103271
  return { statusCode: 403 };
103188
103272
  }
103189
103273
  parts = path25.split(sep2);
103190
- path25 = normalize2(join21(root, path25));
103274
+ path25 = normalize2(join22(root, path25));
103191
103275
  } else {
103192
103276
  if (UP_PATH_REGEXP.test(path25)) {
103193
103277
  debug('malicious path "%s"', path25);
@@ -103471,7 +103555,7 @@ var require_send = __commonJS({
103471
103555
  let err;
103472
103556
  for (let i = 0; i < options.index.length; i++) {
103473
103557
  const index = options.index[i];
103474
- const p = join21(path25, index);
103558
+ const p = join22(path25, index);
103475
103559
  const { error: error49, stat: stat4 } = await tryStat(p);
103476
103560
  if (error49) {
103477
103561
  err = error49;
@@ -111887,6 +111971,13 @@ function setupWatcher(ctx, server) {
111887
111971
  ctx.wss.close();
111888
111972
  });
111889
111973
  }
111974
+ const configFilePath = path17.join(ctx.workspaceRoot, ".kanban.json");
111975
+ const configWatcher = esm_default.watch(configFilePath, {
111976
+ ignoreInitial: true,
111977
+ awaitWriteFinish: { stabilityThreshold: 100 }
111978
+ });
111979
+ configWatcher.on("change", () => handleFileChange(ctx, debounceRef));
111980
+ server.on("close", () => configWatcher.close());
111890
111981
  }
111891
111982
  var fs10, path17;
111892
111983
  var init_watcherSetup = __esm({
@@ -112092,6 +112183,23 @@ var init_lifecycle = __esm({
112092
112183
  });
112093
112184
 
112094
112185
  // src/standalone/internal/runtime.ts
112186
+ function getIndexHtml(basePath = "") {
112187
+ return `<!DOCTYPE html>
112188
+ <html lang="en">
112189
+ <head>
112190
+ <meta charset="UTF-8">
112191
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
112192
+ <link rel="icon" type="image/svg+xml" href="${basePath}/favicon.svg">
112193
+ <link href="${basePath}/style.css" rel="stylesheet">
112194
+ <title>Kanban Board</title>
112195
+ <script>window.__KB_BASE__ = ${JSON.stringify(basePath)}</script>
112196
+ </head>
112197
+ <body>
112198
+ <div id="root"></div>
112199
+ <script type="module" src="${basePath}/index.js"></script>
112200
+ </body>
112201
+ </html>`;
112202
+ }
112095
112203
  function resolveStandaloneWebviewDir(webviewDir) {
112096
112204
  if (webviewDir)
112097
112205
  return webviewDir;
@@ -112105,12 +112213,12 @@ function resolveStandaloneWebviewDir(webviewDir) {
112105
112213
  }
112106
112214
  return candidates[0];
112107
112215
  }
112108
- function createStandaloneRuntime(kanbanDir, webviewDir, httpServer) {
112216
+ function createStandaloneRuntime(kanbanDir, webviewDir, httpServer, basePath) {
112109
112217
  const absoluteKanbanDir = path19.resolve(kanbanDir);
112110
112218
  const workspaceRoot = path19.dirname(absoluteKanbanDir);
112111
112219
  const resolvedWebviewDir = resolveStandaloneWebviewDir(webviewDir);
112112
112220
  const server = httpServer ?? http.createServer();
112113
- const wss = new import_websocket_server.default({ server, path: "/ws" });
112221
+ const wss = new import_websocket_server.default({ server, path: (basePath || "") + "/ws" });
112114
112222
  const ctx = {};
112115
112223
  const sdk = new KanbanSDK(absoluteKanbanDir, {
112116
112224
  onEvent: (event, data) => {
@@ -112159,20 +112267,7 @@ var init_runtime = __esm({
112159
112267
  init_wrapper();
112160
112268
  init_KanbanSDK();
112161
112269
  init_broadcastService();
112162
- indexHtml = `<!DOCTYPE html>
112163
- <html lang="en">
112164
- <head>
112165
- <meta charset="UTF-8">
112166
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
112167
- <link rel="icon" type="image/svg+xml" href="/favicon.svg">
112168
- <link href="/style.css" rel="stylesheet">
112169
- <title>Kanban Board</title>
112170
- </head>
112171
- <body>
112172
- <div id="root"></div>
112173
- <script type="module" src="/index.js"></script>
112174
- </body>
112175
- </html>`;
112270
+ indexHtml = getIndexHtml();
112176
112271
  }
112177
112272
  });
112178
112273
 
@@ -114246,9 +114341,14 @@ var init_messageHandlers = __esm({
114246
114341
  });
114247
114342
 
114248
114343
  // src/standalone/internal/websocket.ts
114249
- function attachWebSocketHandlers(ctx) {
114250
- ctx.wss.on("connection", (ws, req) => {
114251
- const authContext = extractAuthContext(req);
114344
+ function attachWebSocketHandlers(ctx, resolveAuthContext) {
114345
+ ctx.wss.on("connection", async (ws, req) => {
114346
+ let authContext;
114347
+ try {
114348
+ authContext = resolveAuthContext ? await resolveAuthContext(req) : extractAuthContext(req);
114349
+ } catch {
114350
+ authContext = extractAuthContext(req);
114351
+ }
114252
114352
  setClientEditingCard(ctx, ws, null);
114253
114353
  ws.on("message", (data) => {
114254
114354
  let message;
@@ -114323,6 +114423,7 @@ function isPageRequest(method, pathname) {
114323
114423
  function collectStandaloneHttpHandlers(requestType, ctx) {
114324
114424
  const plugins = ctx.sdk.capabilities?.standaloneHttpPlugins ?? [];
114325
114425
  const registrationOptions = {
114426
+ sdk: ctx.sdk,
114326
114427
  workspaceRoot: ctx.workspaceRoot,
114327
114428
  kanbanDir: ctx.absoluteKanbanDir,
114328
114429
  capabilities: ctx.sdk.capabilities?.providers ?? {
@@ -114380,10 +114481,13 @@ function startServer(kanbanDir, port, webviewDir, resolvedConfigPath) {
114380
114481
  const swaggerUiStaticDir = resolveSwaggerUiStaticDir();
114381
114482
  const swaggerUiLogoPath = swaggerUiStaticDir ? path21.join(swaggerUiStaticDir, "logo.svg") : void 0;
114382
114483
  const swaggerUiLogo = swaggerUiLogoPath && fs15.existsSync(swaggerUiLogoPath) ? { type: "image/svg+xml", content: fs15.readFileSync(swaggerUiLogoPath) } : null;
114383
- const runtime = createStandaloneRuntime(kanbanDir, webviewDir, fastify.server);
114484
+ const workspaceRoot = path21.dirname(path21.resolve(kanbanDir));
114485
+ const config3 = readConfig(workspaceRoot);
114486
+ const rawBase = config3.basePath ?? "";
114487
+ const basePath = rawBase ? (rawBase.startsWith("/") ? rawBase : "/" + rawBase).replace(/\/+$/, "") : "";
114488
+ const runtime = createStandaloneRuntime(kanbanDir, webviewDir, fastify.server, basePath);
114384
114489
  const { ctx, resolvedWebviewDir } = runtime;
114385
- const config3 = readConfig(ctx.workspaceRoot);
114386
- let resolvedIndexHtml = indexHtml;
114490
+ let resolvedIndexHtml = getIndexHtml(basePath);
114387
114491
  let customHead = config3.customHeadHtml || "";
114388
114492
  if (config3.customHeadHtmlFile) {
114389
114493
  try {
@@ -114393,14 +114497,14 @@ function startServer(kanbanDir, port, webviewDir, resolvedConfigPath) {
114393
114497
  }
114394
114498
  }
114395
114499
  if (customHead) {
114396
- resolvedIndexHtml = indexHtml.replace("</head>", `${customHead}
114500
+ resolvedIndexHtml = resolvedIndexHtml.replace("</head>", `${customHead}
114397
114501
  </head>`);
114398
114502
  }
114399
114503
  const standaloneHttpPlugins = ctx.sdk.capabilities?.standaloneHttpPlugins ?? [];
114400
114504
  const standaloneOpenApiSpec = buildStandaloneOpenApiSpec(standaloneHttpPlugins);
114401
114505
  fastify.register(import_swagger.default, { openapi: standaloneOpenApiSpec });
114402
114506
  fastify.register(import_swagger_ui.default, {
114403
- routePrefix: "/api/docs",
114507
+ routePrefix: `${basePath}/api/docs`,
114404
114508
  uiConfig: { docExpansion: "list", deepLinking: false },
114405
114509
  logo: swaggerUiLogo,
114406
114510
  ...swaggerUiStaticDir ? { baseDir: swaggerUiStaticDir } : {}
@@ -114432,6 +114536,14 @@ function startServer(kanbanDir, port, webviewDir, resolvedConfigPath) {
114432
114536
  if (request.body instanceof Buffer && request.body.length > 0) {
114433
114537
  req._rawBody = request.body;
114434
114538
  }
114539
+ if (basePath) {
114540
+ const rawUrl = req.url ?? "/";
114541
+ if (rawUrl === basePath) {
114542
+ req.url = "/";
114543
+ } else if (rawUrl.startsWith(basePath + "/") || rawUrl.startsWith(basePath + "?")) {
114544
+ req.url = rawUrl.slice(basePath.length);
114545
+ }
114546
+ }
114435
114547
  const requestContext = createRequestContext(ctx, req, reply.raw, resolvedWebviewDir, resolvedIndexHtml);
114436
114548
  await dispatchRequest(requestContext, middlewareHandlers);
114437
114549
  if (!reply.sent && !reply.raw.writableEnded) {
@@ -114439,7 +114551,62 @@ function startServer(kanbanDir, port, webviewDir, resolvedConfigPath) {
114439
114551
  }
114440
114552
  reply.hijack();
114441
114553
  });
114442
- attachWebSocketHandlers(ctx);
114554
+ const resolveWsAuthContext = async (req) => {
114555
+ const silentRes = /* @__PURE__ */ (() => {
114556
+ const r = {
114557
+ writableEnded: false,
114558
+ writeHead() {
114559
+ return r;
114560
+ },
114561
+ setHeader() {
114562
+ return r;
114563
+ },
114564
+ removeHeader() {
114565
+ },
114566
+ getHeader() {
114567
+ return void 0;
114568
+ },
114569
+ getHeaders() {
114570
+ return {};
114571
+ },
114572
+ end(..._args) {
114573
+ r.writableEnded = true;
114574
+ return r;
114575
+ },
114576
+ write() {
114577
+ return false;
114578
+ }
114579
+ };
114580
+ return r;
114581
+ })();
114582
+ const reqWithBody = req;
114583
+ const wsUrl = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
114584
+ const requestContext = {
114585
+ ctx,
114586
+ sdk: ctx.sdk,
114587
+ workspaceRoot: ctx.workspaceRoot,
114588
+ kanbanDir: ctx.absoluteKanbanDir,
114589
+ req: reqWithBody,
114590
+ res: silentRes,
114591
+ url: wsUrl,
114592
+ pathname: wsUrl.pathname,
114593
+ method: "GET",
114594
+ resolvedWebviewDir,
114595
+ indexHtml: resolvedIndexHtml,
114596
+ route: createRouteMatcher("GET", wsUrl.pathname, matchRoute),
114597
+ isApiRequest: false,
114598
+ isPageRequest: false,
114599
+ getAuthContext: () => getRequestAuthContext(req),
114600
+ setAuthContext: (auth) => setRequestAuthContext(req, auth),
114601
+ mergeAuthContext: (auth) => mergeRequestAuthContext(req, auth)
114602
+ };
114603
+ for (const handler of middlewareHandlers) {
114604
+ if (await handler(requestContext))
114605
+ break;
114606
+ }
114607
+ return extractAuthContext(req);
114608
+ };
114609
+ attachWebSocketHandlers(ctx, resolveWsAuthContext);
114443
114610
  setupStandaloneLifecycle(ctx, fastify.server);
114444
114611
  const effectiveConfigPath = resolvedConfigPath ?? configPath(path21.dirname(ctx.absoluteKanbanDir));
114445
114612
  fastify.listen({ port, host: "0.0.0.0" }, (err) => {
@@ -114447,7 +114614,7 @@ function startServer(kanbanDir, port, webviewDir, resolvedConfigPath) {
114447
114614
  console.error("Failed to start server:", err);
114448
114615
  process.exit(1);
114449
114616
  }
114450
- console.log(`Kanban board running at http://localhost:${port}`);
114617
+ console.log(`Kanban board running at http://localhost:${port}${basePath}`);
114451
114618
  console.log(`API available at http://localhost:${port}/api`);
114452
114619
  console.log(`Kanban config: ${effectiveConfigPath}`);
114453
114620
  console.log(`Kanban directory: ${ctx.absoluteKanbanDir}`);
@@ -116869,7 +117036,7 @@ async function cmdAuth(sdk, positional, flags, cliPlugins, workspaceRoot) {
116869
117036
  if (sub !== "status") {
116870
117037
  const authPlugin = findCliPlugin(cliPlugins, "auth");
116871
117038
  if (authPlugin) {
116872
- await authPlugin.run(positional, flags, { workspaceRoot });
117039
+ await runCliPlugin(authPlugin, positional, flags, workspaceRoot, sdk);
116873
117040
  return;
116874
117041
  }
116875
117042
  console.error(red(`Unknown auth sub-command: ${sub}`));