@ivotoby/postgram-cli 1.40.0 → 1.41.0

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/dist/client.js CHANGED
@@ -120,6 +120,9 @@ export function createPgmClient(options) {
120
120
  },
121
121
  listTasks(input = {}) {
122
122
  const params = new URLSearchParams();
123
+ if (input.kind) {
124
+ params.set('kind', input.kind);
125
+ }
123
126
  if (input.status) {
124
127
  params.set('status', input.status);
125
128
  }
package/dist/pgm.js CHANGED
@@ -616,6 +616,7 @@ taskCommand
616
616
  await runWithClient(command, async (client, json) => {
617
617
  validateToonOptions(options, json);
618
618
  const body = await client.listTasks({
619
+ kind: 'task',
619
620
  status: options.status,
620
621
  context: options.context,
621
622
  limit: Number(options.limit),
@@ -40,7 +40,7 @@ async function resolveTaskId(id, deps) {
40
40
  for (let offset = 0;; offset += 100) {
41
41
  const page = await deps.pgm.listTasks({ limit: 100, offset });
42
42
  for (const item of page.items) {
43
- if (item.id.startsWith(prefix)) {
43
+ if (item.id.startsWith(prefix) && item.metadata.kind === 'feature') {
44
44
  matches.push(item.id);
45
45
  }
46
46
  }
@@ -64,6 +64,9 @@ async function resolveTaskId(id, deps) {
64
64
  export async function runFeatureDone(id, deps) {
65
65
  const taskId = await resolveTaskId(id, deps);
66
66
  const current = await deps.pgm.recallEntity(taskId);
67
+ if (current.entity.metadata.kind !== 'feature') {
68
+ throw new UsageError(`${shortId(taskId)} is not a feature`);
69
+ }
67
70
  if (current.entity.status === 'done') {
68
71
  return deps.json
69
72
  ? { id: taskId, status: 'done', changed: false }
@@ -3,6 +3,8 @@ import { execFile } from 'node:child_process';
3
3
  import { AppError, ErrorCode } from '../errors.js';
4
4
  import { shortId } from '../shared.js';
5
5
  import { UsageError } from './config.js';
6
+ const FULL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
7
+ const SHORT_HEX = /^[0-9a-f]{8}$/i;
6
8
  const GH_STATES = {
7
9
  OPEN: 'open',
8
10
  MERGED: 'merged',
@@ -78,10 +80,16 @@ export async function runFeatureSync(id, deps) {
78
80
  let features = collectFeatures(response);
79
81
  if (id !== undefined) {
80
82
  const wanted = id.toLowerCase();
81
- features = features.filter((feature) => feature.id === wanted || feature.id.startsWith(wanted));
83
+ if (!FULL_UUID.test(wanted) && !SHORT_HEX.test(wanted)) {
84
+ throw new UsageError('feature id must be a full UUID or an 8-hex short id');
85
+ }
86
+ features = features.filter((feature) => FULL_UUID.test(wanted) ? feature.id === wanted : feature.id.startsWith(wanted));
82
87
  if (features.length === 0) {
83
88
  throw new AppError(ErrorCode.NOT_FOUND, `no feature matches ${id}`);
84
89
  }
90
+ if (features.length > 1) {
91
+ throw new AppError(ErrorCode.CONFLICT, `feature id ${id} is ambiguous`);
92
+ }
85
93
  }
86
94
  const result = { checked: 0, unchanged: 0, changed: [], errors: [] };
87
95
  const answers = new Map();
@@ -146,6 +154,6 @@ export function formatSync(result) {
146
154
  for (const error of result.errors) {
147
155
  lines.push(`error ${shortId(error.feature_id)} ${error.url}: ${error.error}`);
148
156
  }
149
- lines.push(`checked ${result.checked} pull requests: ${result.changed.length} changed, ${result.unchanged} unchanged, ${result.errors.length} failed`);
157
+ lines.push(`checked ${result.checked} pull ${result.checked === 1 ? 'request' : 'requests'}: ${result.changed.length} changed, ${result.unchanged} unchanged, ${result.errors.length} failed`);
150
158
  return lines;
151
159
  }
@@ -149,6 +149,13 @@ export async function runHere(options, deps) {
149
149
  if (isIgnoredPath(local.path, config.ignorePaths)) {
150
150
  return ignoredResult(options, local.path);
151
151
  }
152
+ // A directory outside git holds no project code, so it is never a workspace.
153
+ if (local.repoRoot === null) {
154
+ return {
155
+ stdout: options.hook ? '' : `skipped ${local.path} (not a git repository)\n`,
156
+ exitCode: 0
157
+ };
158
+ }
152
159
  const marker = await deps.readMarker(local.path);
153
160
  const bootId = await deps.bootId();
154
161
  const session = sessionId !== null || event !== null
@@ -11,6 +11,16 @@ import { readMarker, resolveLocalWorkspace, runGit, writeMarker } from './local.
11
11
  import { discoverWorktrees, findClaudeSessions, findPiSessions } from './scan-discovery.js';
12
12
  import { syncMissing } from './where.js';
13
13
  export const BRANCH_SHORT_ID = /(?:^|\/)([0-9a-f]{8})(?:-|$)/;
14
+ /** Keeps the feature tasks of a task listing, so plain tasks are never offered or linked. */
15
+ export function featureTasks(items) {
16
+ return items
17
+ .filter((item) => item.metadata.kind === 'feature')
18
+ .map((item) => ({
19
+ id: item.id,
20
+ title: firstLineTitle(item.content),
21
+ done: item.status === 'done'
22
+ }));
23
+ }
14
24
  const DEFAULT_DEPTH = 3;
15
25
  function parseDepth(raw) {
16
26
  if (raw === undefined) {
@@ -57,7 +67,9 @@ async function buildProposals(worktree, deps, tasks, yesExact) {
57
67
  catch {
58
68
  hits = [];
59
69
  }
60
- for (const hit of hits.slice(0, 3)) {
70
+ // Search covers every task; only open features are worth proposing.
71
+ const open = new Set(tasks.filter((task) => task.done !== true).map((task) => task.id));
72
+ for (const hit of hits.filter((h) => open.has(h.id)).slice(0, 3)) {
61
73
  proposals.push({ kind: 'search', featureId: hit.id, title: hit.title });
62
74
  }
63
75
  }
@@ -519,9 +531,7 @@ export function registerScanCommand(program) {
519
531
  const items = [];
520
532
  for (let offset = 0;; offset += 100) {
521
533
  const page = await context.pgm.listTasks({ limit: 100, offset });
522
- for (const item of page.items) {
523
- items.push({ id: item.id, title: firstLineTitle(item.content) });
524
- }
534
+ items.push(...featureTasks(page.items));
525
535
  if (page.items.length < 100) {
526
536
  break;
527
537
  }
@@ -532,7 +542,7 @@ export function registerScanCommand(program) {
532
542
  const response = await context.pgm.searchEntities({
533
543
  query,
534
544
  type: 'task',
535
- limit: 3
545
+ limit: 10
536
546
  });
537
547
  return response.results.map((result) => ({
538
548
  id: result.entity.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ivotoby/postgram-cli",
3
- "version": "1.40.0",
3
+ "version": "1.41.0",
4
4
  "description": "Postgram CLI — store, search, and manage entities from the command line",
5
5
  "type": "module",
6
6
  "bin": {