kankaku 1.3.1 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +0 -0
- package/package.json +3 -3
- package/vendor/kankaku-pi/package.json +1 -1
- package/vendor/kankaku-pi/src/adapters/cached-catalog.ts +19 -3
- package/vendor/kankaku-pi/src/adapters/kankaku-command.ts +17 -3
- package/vendor/kankaku-pi/src/adapters/pocketbase-catalog.ts +17 -0
- package/vendor/kankaku-pi/src/adapters/session-target.ts +28 -6
- package/vendor/kankaku-pi/src/adapters/target-picker.ts +34 -6
- package/vendor/kankaku-pi/src/extension.ts +8 -2
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ left untouched — and installs the slash commands:
|
|
|
115
115
|
`--plugin-dir` flag needed**.
|
|
116
116
|
|
|
117
117
|
It also installs the `/kankaku:*` slash commands (`/kankaku:report`,
|
|
118
|
-
`/kankaku:status`, `/kankaku:task`, `/kankaku:sync`, …) as user commands under
|
|
118
|
+
`/kankaku:status`, `/kankaku:task`, `/kankaku:target`, `/kankaku:sync`, …) as user commands under
|
|
119
119
|
`~/.claude/commands/kankaku/<name>.md`, generated from the plugin's own
|
|
120
120
|
`commands/*.md` with the plugin's absolute install path filled in. Setup
|
|
121
121
|
lists each file it wrote or left unchanged; `kankaku doctor` reports Claude
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kankaku",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Terminal app that shows today's kankaku work across every project on disk",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"ink": "^7.1.1",
|
|
41
|
-
"kankaku-
|
|
42
|
-
"kankaku-claude": "^1.3.1",
|
|
41
|
+
"kankaku-claude": "^1.4.1",
|
|
43
42
|
"kankaku-hub": "^0.3.0",
|
|
43
|
+
"kankaku-pi": "^1.4.1",
|
|
44
44
|
"react": "^19.3.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
@@ -61,6 +61,7 @@ export class CachedCatalog implements Catalog {
|
|
|
61
61
|
private readonly deps: CachedCatalogDeps;
|
|
62
62
|
private memo: CatalogSnapshot | undefined;
|
|
63
63
|
private memoized = false;
|
|
64
|
+
private pendingAdoptions = new Set<Map<string, HubTask>>();
|
|
64
65
|
|
|
65
66
|
constructor(deps: CachedCatalogDeps) {
|
|
66
67
|
this.deps = deps;
|
|
@@ -74,6 +75,16 @@ export class CachedCatalog implements Catalog {
|
|
|
74
75
|
return this.memo;
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
/** Adopt a POST response without relying on an eventually-consistent list refresh. */
|
|
79
|
+
adoptTask(task: HubTask): void {
|
|
80
|
+
const snapshot = this.read();
|
|
81
|
+
if (!snapshot) throw new Error("Cannot adopt a task without a catalog snapshot");
|
|
82
|
+
const adopted = { ...snapshot, tasks: [...(snapshot.tasks ?? []).filter((item) => item.id !== task.id), task] };
|
|
83
|
+
this.writeDisk(adopted, true);
|
|
84
|
+
this.memo = adopted;
|
|
85
|
+
for (const pending of this.pendingAdoptions) pending.set(task.id, task);
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
isStale(): boolean {
|
|
78
89
|
const snapshot = this.read();
|
|
79
90
|
if (!snapshot) return true;
|
|
@@ -82,6 +93,8 @@ export class CachedCatalog implements Catalog {
|
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
async refresh(signal?: AbortSignal): Promise<CatalogSnapshot | undefined> {
|
|
96
|
+
const adopted = new Map<string, HubTask>();
|
|
97
|
+
this.pendingAdoptions.add(adopted);
|
|
85
98
|
try {
|
|
86
99
|
const { clients, projects, tasks } = await this.deps.fetchCatalog(signal);
|
|
87
100
|
const snapshot: CatalogSnapshot = {
|
|
@@ -89,7 +102,7 @@ export class CachedCatalog implements Catalog {
|
|
|
89
102
|
url: this.deps.url,
|
|
90
103
|
clients,
|
|
91
104
|
projects,
|
|
92
|
-
...(tasks !== undefined ? { tasks } : {}),
|
|
105
|
+
...(tasks !== undefined || adopted.size > 0 ? { tasks: [...(tasks ?? []).filter((task) => !adopted.has(task.id)), ...adopted.values()] } : {}),
|
|
93
106
|
};
|
|
94
107
|
this.writeDisk(snapshot);
|
|
95
108
|
this.memo = snapshot;
|
|
@@ -97,6 +110,8 @@ export class CachedCatalog implements Catalog {
|
|
|
97
110
|
return snapshot;
|
|
98
111
|
} catch {
|
|
99
112
|
return undefined;
|
|
113
|
+
} finally {
|
|
114
|
+
this.pendingAdoptions.delete(adopted);
|
|
100
115
|
}
|
|
101
116
|
}
|
|
102
117
|
|
|
@@ -112,7 +127,7 @@ export class CachedCatalog implements Catalog {
|
|
|
112
127
|
}
|
|
113
128
|
}
|
|
114
129
|
|
|
115
|
-
private writeDisk(snapshot: CatalogSnapshot): void {
|
|
130
|
+
private writeDisk(snapshot: CatalogSnapshot, required = false): void {
|
|
116
131
|
try {
|
|
117
132
|
// This cache lives under `~/.kankaku` (machine-wide, not a project's
|
|
118
133
|
// own KANKAKU_DIR) — but `~/.kankaku` itself is never mode-tightened
|
|
@@ -139,7 +154,8 @@ export class CachedCatalog implements Catalog {
|
|
|
139
154
|
// Owner-only: this file names every client/project the machine's user has touched.
|
|
140
155
|
writeFileSync(tmp, JSON.stringify(snapshot), { mode: OWNER_FILE_MODE });
|
|
141
156
|
renameSync(tmp, this.deps.filePath);
|
|
142
|
-
} catch {
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (required) throw error;
|
|
143
159
|
// Best-effort cache write: a failure here must not fail the refresh
|
|
144
160
|
// itself, since the in-memory snapshot is still usable this process.
|
|
145
161
|
}
|
|
@@ -194,7 +194,7 @@ const COMMAND_TOKENS = ["all", "tasks", "sessions", "client", "clients", "export
|
|
|
194
194
|
/** Only offered when the hub is configured, so completions are unchanged for users without one. */
|
|
195
195
|
const HUB_COMMAND_TOKENS = ["target", "task", "projects", "catalog", "sync", "backfill"];
|
|
196
196
|
const TARGET_TOKENS = ["pick", "clear"];
|
|
197
|
-
const TASK_TOKENS = ["pick", "clear"];
|
|
197
|
+
const TASK_TOKENS = ["pick", "clear", "new"];
|
|
198
198
|
const CATALOG_TOKENS = ["refresh"];
|
|
199
199
|
const SYNC_TOKENS = ["all", "status"];
|
|
200
200
|
|
|
@@ -480,7 +480,7 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
480
480
|
}
|
|
481
481
|
|
|
482
482
|
/**
|
|
483
|
-
* Handle `/kankaku task [pick|clear]`; `rest` excludes the leading `task`
|
|
483
|
+
* Handle `/kankaku task [pick|clear|new <title> [--ref <externalRef>]]`; `rest` excludes the leading `task`
|
|
484
484
|
* token. Default (no args) is `pick`. `pickTask`/`clearTask`
|
|
485
485
|
* (`session-target.ts`) notify their own outcome directly, so this only
|
|
486
486
|
* dispatches and refreshes the idle status line.
|
|
@@ -505,6 +505,19 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
505
505
|
return;
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
+
if (rest[0] === "new") {
|
|
509
|
+
const refIndex = rest.indexOf("--ref");
|
|
510
|
+
const title = rest.slice(1, refIndex === -1 ? undefined : refIndex).join(" ").trim();
|
|
511
|
+
const externalRef = refIndex === -1 ? undefined : rest[refIndex + 1]?.trim();
|
|
512
|
+
if (!title || (refIndex !== -1 && (!externalRef || refIndex !== rest.length - 2))) {
|
|
513
|
+
notifyError(ctx, new Error("usage: /kankaku task new <title> [--ref <externalRef>]"));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
await sessionTarget.newTask(pi, ctx, title, externalRef);
|
|
517
|
+
deps.refreshIdleStatus(ctx);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
508
521
|
notifyError(ctx, new Error(`unknown task subcommand: ${rest.join(" ")}`));
|
|
509
522
|
}
|
|
510
523
|
|
|
@@ -598,7 +611,8 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
598
611
|
"When a hub (PocketBase) is configured: 'target' to show the effective client/project and its source, " +
|
|
599
612
|
"'target pick' to run the picker again, 'target clear' to clear the session target, " +
|
|
600
613
|
"'task' (or 'task pick') to link this session to an open/doing hub task of the effective project, " +
|
|
601
|
-
"'task clear' to drop the link, " +
|
|
614
|
+
"'task clear' to drop the link, 'task new <title> [--ref <externalRef>]' to create and link an open task; " +
|
|
615
|
+
"the visual task picker also offers creation, " +
|
|
602
616
|
"'catalog refresh' to force a catalog refresh, 'projects' for per-project totals today ('projects all' for every day), " +
|
|
603
617
|
"'sync' to push pending tasks to the hub ('sync all' for a full re-evaluation, 'sync status' for the watermark/pending count/last error), " +
|
|
604
618
|
"'backfill' to run a full sync and report how many tasks went to Sin determinar, grouped by their old label. " +
|
|
@@ -23,6 +23,23 @@ interface TaskRecord extends PocketBaseRecord {
|
|
|
23
23
|
external_ref?: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export interface CreateHubTaskInput {
|
|
27
|
+
title: string;
|
|
28
|
+
projectId: string;
|
|
29
|
+
externalRef?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Explicit user-driven creation; never called from hook paths. */
|
|
33
|
+
export async function createHubTask(client: PocketBaseClient, input: CreateHubTaskInput, signal?: AbortSignal): Promise<HubTask> {
|
|
34
|
+
const record = await client.request<TaskRecord>("POST", "/api/collections/tasks/records", {
|
|
35
|
+
title: input.title,
|
|
36
|
+
project: input.projectId,
|
|
37
|
+
status: "open",
|
|
38
|
+
...(input.externalRef !== undefined ? { external_ref: input.externalRef } : {}),
|
|
39
|
+
}, signal);
|
|
40
|
+
return mapTask(record);
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
const TASK_STATUSES = new Set(["open", "doing", "done"]);
|
|
27
44
|
|
|
28
45
|
function mapClient(record: ClientRecord): Client {
|
|
@@ -3,6 +3,7 @@ import { formatWorkTargetLabel, resolveWorkTarget, resolveWorkTargetSource } fro
|
|
|
3
3
|
import type { WorkTarget, WorkTargetCandidate, WorkTargetSessionOverride, WorkTargetSourceName } from "../domain/work-target.ts";
|
|
4
4
|
import type { WorkRole } from "../domain/work-record.ts";
|
|
5
5
|
import type { Catalog, CatalogSnapshot } from "../ports/catalog.ts";
|
|
6
|
+
import type { TaskCreator } from "./target-picker.ts";
|
|
6
7
|
import { pickHubTask, pickTarget } from "./target-picker.ts";
|
|
7
8
|
|
|
8
9
|
/** Persisted as a `kankaku-target` custom session entry so the session-level target survives a reload. */
|
|
@@ -14,8 +15,7 @@ export interface KankakuTargetEntryData {
|
|
|
14
15
|
/**
|
|
15
16
|
* A hub task picked for this session (`/kankaku task pick`). Session-only:
|
|
16
17
|
* never written to the project's `.kankaku/config.json`, and dropped by
|
|
17
|
-
*
|
|
18
|
-
* silent resolution) since those all build a fresh candidate without it.
|
|
18
|
+
* target changes unless a new task is selected in the target picker.
|
|
19
19
|
*/
|
|
20
20
|
hubTaskId?: string;
|
|
21
21
|
}
|
|
@@ -25,6 +25,8 @@ export const TARGET_ENTRY_TYPE = "kankaku-target";
|
|
|
25
25
|
export interface SessionTargetDeps {
|
|
26
26
|
role: WorkRole;
|
|
27
27
|
catalog: Catalog;
|
|
28
|
+
/** Creates and adopts the returned task before linking it. */
|
|
29
|
+
createTask?: TaskCreator;
|
|
28
30
|
/** Lazily reads `clientId`/`projectId` from `<kankaku dir>/config.json`. */
|
|
29
31
|
resolveProjectConfigIds: () => WorkTargetCandidate | undefined;
|
|
30
32
|
/** Persist `clientId`/`projectId` into `<kankaku dir>/config.json`, merging existing keys. */
|
|
@@ -81,6 +83,8 @@ export interface SessionTarget {
|
|
|
81
83
|
pickTask(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void>;
|
|
82
84
|
/** `/kankaku task clear`: drops the linked hub task, keeping the rest of the session target. A no-op when none is linked. */
|
|
83
85
|
clearTask(pi: ExtensionAPI): void;
|
|
86
|
+
/** Create through the shared creator and link only in the current session. */
|
|
87
|
+
newTask(pi: ExtensionAPI, ctx: ExtensionContext, title: string, externalRef?: string): Promise<void>;
|
|
84
88
|
/**
|
|
85
89
|
* Set the linked hub task directly, bypassing the picker — the panel's
|
|
86
90
|
* target screen (`adapters/panel/screens/target.ts`) uses this once the
|
|
@@ -291,7 +295,7 @@ export function createSessionTarget(deps: SessionTargetDeps): SessionTarget {
|
|
|
291
295
|
}
|
|
292
296
|
|
|
293
297
|
async function runPicker(pi: ExtensionAPI, ctx: ExtensionContext, snapshot: CatalogSnapshot): Promise<void> {
|
|
294
|
-
const result = await pickTarget(ctx, snapshot);
|
|
298
|
+
const result = await pickTarget(ctx, snapshot, deps.createTask);
|
|
295
299
|
|
|
296
300
|
if (result.kind === "skipped") {
|
|
297
301
|
sessionOverride = "skipped";
|
|
@@ -299,13 +303,17 @@ export function createSessionTarget(deps: SessionTargetDeps): SessionTarget {
|
|
|
299
303
|
return;
|
|
300
304
|
}
|
|
301
305
|
|
|
302
|
-
const
|
|
306
|
+
const projectIds = candidateFrom(result.target);
|
|
307
|
+
const ids: WorkTargetCandidate = {
|
|
308
|
+
...projectIds,
|
|
309
|
+
...(result.target.hubTaskId !== undefined ? { hubTaskId: result.target.hubTaskId } : {}),
|
|
310
|
+
};
|
|
303
311
|
sessionOverride = ids;
|
|
304
312
|
pi.appendEntry<KankakuTargetEntryData>(TARGET_ENTRY_TYPE, entryDataFrom(ids));
|
|
305
313
|
|
|
306
314
|
const remember = await ctx.ui.confirm("kankaku", `Remember ${formatWorkTargetLabel(result.target)} for this repository?`);
|
|
307
315
|
if (remember) {
|
|
308
|
-
deps.persistProjectConfig(
|
|
316
|
+
deps.persistProjectConfig(projectIds);
|
|
309
317
|
}
|
|
310
318
|
}
|
|
311
319
|
|
|
@@ -390,7 +398,7 @@ export function createSessionTarget(deps: SessionTargetDeps): SessionTarget {
|
|
|
390
398
|
}
|
|
391
399
|
|
|
392
400
|
const tasks = deps.catalog.read()?.tasks ?? [];
|
|
393
|
-
const result = await pickHubTask(ctx, tasks, target.projectId);
|
|
401
|
+
const result = await pickHubTask(ctx, tasks, target.projectId, deps.createTask);
|
|
394
402
|
|
|
395
403
|
if (result.kind === "empty") {
|
|
396
404
|
ctx.ui.notify(`kankaku: No open tasks for ${target.projectName ?? target.projectId} in the hub`, "warning");
|
|
@@ -404,6 +412,19 @@ export function createSessionTarget(deps: SessionTargetDeps): SessionTarget {
|
|
|
404
412
|
if (updated) ctx.ui.notify(`kankaku: task set to ${formatWorkTargetLabel(updated)}`);
|
|
405
413
|
}
|
|
406
414
|
|
|
415
|
+
async function newTask(pi: ExtensionAPI, ctx: ExtensionContext, title: string, externalRef?: string): Promise<void> {
|
|
416
|
+
if (deps.role !== "orchestrator" || !ctx.hasUI) return;
|
|
417
|
+
const target = effectiveTarget();
|
|
418
|
+
if (!target || target.projectId === undefined) {
|
|
419
|
+
ctx.ui.notify("kankaku: Pick a client and project first (/kankaku target pick)", "warning");
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (!deps.createTask) throw new Error("task creation is not configured");
|
|
423
|
+
const task = await deps.createTask(target.projectId, title, externalRef);
|
|
424
|
+
setTask(pi, task.id);
|
|
425
|
+
ctx.ui.notify(`kankaku: created and linked: ${task.title}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
407
428
|
function clearTask(pi: ExtensionAPI): void {
|
|
408
429
|
if (sessionOverride === undefined || sessionOverride === "skipped") return;
|
|
409
430
|
if (sessionOverride.hubTaskId === undefined) return;
|
|
@@ -420,6 +441,7 @@ export function createSessionTarget(deps: SessionTargetDeps): SessionTarget {
|
|
|
420
441
|
clear,
|
|
421
442
|
pickTask,
|
|
422
443
|
clearTask,
|
|
444
|
+
newTask,
|
|
423
445
|
setTask,
|
|
424
446
|
rememberTarget,
|
|
425
447
|
effectiveTarget,
|
|
@@ -3,10 +3,13 @@ import type { Client, HubTask, Project, WorkTarget } from "../domain/work-target
|
|
|
3
3
|
|
|
4
4
|
const SKIP_OPTION = "— skip —";
|
|
5
5
|
const NO_PROJECT_OPTION = "(no project)";
|
|
6
|
+
const CREATE_OPTION = "+ create task +";
|
|
7
|
+
export type TaskCreator = (projectId: string, title: string, externalRef?: string) => Promise<HubTask>;
|
|
6
8
|
|
|
7
9
|
export interface PickerCatalog {
|
|
8
10
|
clients: Client[];
|
|
9
11
|
projects: Project[];
|
|
12
|
+
tasks?: HubTask[];
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
export type PickResult = { kind: "picked"; target: WorkTarget } | { kind: "skipped" };
|
|
@@ -36,14 +39,15 @@ function labelOptions<T extends { name: string; code?: string }>(items: T[]): La
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
/**
|
|
39
|
-
* Run the client/project picker (`ctx.ui.select`) against an already-loaded
|
|
42
|
+
* Run the client/project/task picker (`ctx.ui.select`) against an already-loaded
|
|
40
43
|
* catalog snapshot. Pure UI interaction: no network, no persistence — the
|
|
41
44
|
* caller (`session-target.ts`) decides what to do with the result.
|
|
42
45
|
*
|
|
43
|
-
* Declining at either step — choosing "— skip —" or dismissing the dialog
|
|
46
|
+
* Declining at either client/project step — choosing "— skip —" or dismissing the dialog
|
|
44
47
|
* (`undefined`) — cancels the whole pick, not just that step.
|
|
48
|
+
* Skipping the optional task step keeps the selected client/project.
|
|
45
49
|
*/
|
|
46
|
-
export async function pickTarget(ctx: ExtensionContext, catalog: PickerCatalog): Promise<PickResult> {
|
|
50
|
+
export async function pickTarget(ctx: ExtensionContext, catalog: PickerCatalog, createTask?: TaskCreator): Promise<PickResult> {
|
|
47
51
|
const pickableClients = catalog.clients.filter((client) => client.active && !client.unassigned);
|
|
48
52
|
const clientOptions = labelOptions(pickableClients);
|
|
49
53
|
|
|
@@ -78,6 +82,14 @@ export async function pickTarget(ctx: ExtensionContext, catalog: PickerCatalog):
|
|
|
78
82
|
: {}),
|
|
79
83
|
};
|
|
80
84
|
|
|
85
|
+
if (project) {
|
|
86
|
+
const taskResult = await pickHubTask(ctx, catalog.tasks ?? [], project.id, createTask);
|
|
87
|
+
if (taskResult.kind === "picked") {
|
|
88
|
+
target.hubTaskId = taskResult.task.id;
|
|
89
|
+
target.hubTaskTitle = taskResult.task.title;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
81
93
|
return { kind: "picked", target };
|
|
82
94
|
}
|
|
83
95
|
|
|
@@ -112,13 +124,29 @@ function labelTaskOptions(tasks: HubTask[]): LabeledOption<HubTask>[] {
|
|
|
112
124
|
* has no open/doing task, so the caller can tell "nothing to pick from"
|
|
113
125
|
* apart from "the user skipped".
|
|
114
126
|
*/
|
|
115
|
-
export async function pickHubTask(ctx: ExtensionContext, tasks: HubTask[], projectId: string): Promise<PickHubTaskResult> {
|
|
127
|
+
export async function pickHubTask(ctx: ExtensionContext, tasks: HubTask[], projectId: string, createTask?: TaskCreator): Promise<PickHubTaskResult> {
|
|
116
128
|
const pickable = tasks.filter((task) => task.status !== "done" && task.projectId === projectId);
|
|
117
|
-
if (pickable.length === 0) return { kind: "empty" };
|
|
129
|
+
if (pickable.length === 0 && !createTask) return { kind: "empty" };
|
|
118
130
|
|
|
119
131
|
const taskOptions = labelTaskOptions(pickable);
|
|
120
|
-
const
|
|
132
|
+
const labels = taskOptions.map((option) => option.label);
|
|
133
|
+
let createOption = CREATE_OPTION;
|
|
134
|
+
for (let suffix = 2; labels.includes(createOption); suffix++) {
|
|
135
|
+
createOption = `${CREATE_OPTION} (${suffix})`;
|
|
136
|
+
}
|
|
137
|
+
const choice = await ctx.ui.select("kankaku — task", [...labels, ...(createTask ? [createOption] : []), SKIP_OPTION]);
|
|
121
138
|
if (choice === undefined || choice === SKIP_OPTION) return { kind: "skipped" };
|
|
139
|
+
if (choice === createOption && createTask) {
|
|
140
|
+
const title = (await ctx.ui.input("kankaku — task title"))?.trim();
|
|
141
|
+
if (!title) return { kind: "skipped" };
|
|
142
|
+
const externalRef = (await ctx.ui.input("kankaku — external reference (optional)"))?.trim();
|
|
143
|
+
try {
|
|
144
|
+
return { kind: "picked", task: await createTask(projectId, title, externalRef || undefined) };
|
|
145
|
+
} catch {
|
|
146
|
+
ctx.ui.notify("kankaku: task creation failed; no task linked", "error");
|
|
147
|
+
return { kind: "skipped" };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
122
150
|
|
|
123
151
|
const task = taskOptions.find((option) => option.label === choice)?.item;
|
|
124
152
|
if (!task) return { kind: "skipped" };
|
|
@@ -11,7 +11,7 @@ import { LazyProjectClientSource, LazyProjectTargetSource } from "./adapters/pro
|
|
|
11
11
|
import { LazyExportWriter } from "./adapters/export-writer.ts";
|
|
12
12
|
import { resolveHubCredentials, safeHomeDir } from "./adapters/hub-credentials.ts";
|
|
13
13
|
import { PocketBaseClient } from "./adapters/pocketbase-client.ts";
|
|
14
|
-
import { createPocketBaseCatalogFetcher } from "./adapters/pocketbase-catalog.ts";
|
|
14
|
+
import { createHubTask, createPocketBaseCatalogFetcher } from "./adapters/pocketbase-catalog.ts";
|
|
15
15
|
import { CachedCatalog } from "./adapters/cached-catalog.ts";
|
|
16
16
|
import { createSessionTarget } from "./adapters/session-target.ts";
|
|
17
17
|
import { resolveKankakuDir, resolveWritableTarget } from "./adapters/kankaku-dir.ts";
|
|
@@ -212,7 +212,7 @@ export default function kankaku(pi: ExtensionAPI): void {
|
|
|
212
212
|
// when no home directory is available so an env-only hub configuration
|
|
213
213
|
// still works without one (the cache just does not survive a reboot).
|
|
214
214
|
const homeDirForCache = safeHomeDir(homedir) ?? tmpdir();
|
|
215
|
-
|
|
215
|
+
const cachedCatalog = new CachedCatalog({
|
|
216
216
|
// Machine-wide cache: several projects on the same machine share one
|
|
217
217
|
// catalog fetch, and it survives across projects.
|
|
218
218
|
filePath: join(homeDirForCache, ".kankaku", "catalog.json"),
|
|
@@ -221,10 +221,16 @@ export default function kankaku(pi: ExtensionAPI): void {
|
|
|
221
221
|
fetchCatalog: createPocketBaseCatalogFetcher(client),
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
+
catalog = cachedCatalog;
|
|
224
225
|
const projectTarget = new LazyProjectTargetSource(config.dir);
|
|
225
226
|
sessionTarget = createSessionTarget({
|
|
226
227
|
role,
|
|
227
228
|
catalog,
|
|
229
|
+
createTask: async (projectId, title, externalRef) => {
|
|
230
|
+
const task = await createHubTask(client, { projectId, title, ...(externalRef ? { externalRef } : {}) });
|
|
231
|
+
cachedCatalog.adoptTask(task);
|
|
232
|
+
return task;
|
|
233
|
+
},
|
|
228
234
|
resolveProjectConfigIds: () => projectTarget.read(),
|
|
229
235
|
persistProjectConfig: (ids) => projectTarget.write(ids),
|
|
230
236
|
});
|