@stoneforge/quarry 0.1.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/LICENSE +13 -0
- package/README.md +160 -0
- package/dist/api/index.d.ts +8 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +8 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/quarry-api.d.ts +268 -0
- package/dist/api/quarry-api.d.ts.map +1 -0
- package/dist/api/quarry-api.js +3905 -0
- package/dist/api/quarry-api.js.map +1 -0
- package/dist/api/types.d.ts +1359 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +204 -0
- package/dist/api/types.js.map +1 -0
- package/dist/bin/sf.d.ts +3 -0
- package/dist/bin/sf.d.ts.map +1 -0
- package/dist/bin/sf.js +9 -0
- package/dist/bin/sf.js.map +1 -0
- package/dist/cli/commands/admin.d.ts +11 -0
- package/dist/cli/commands/admin.d.ts.map +1 -0
- package/dist/cli/commands/admin.js +465 -0
- package/dist/cli/commands/admin.js.map +1 -0
- package/dist/cli/commands/alias.d.ts +8 -0
- package/dist/cli/commands/alias.d.ts.map +1 -0
- package/dist/cli/commands/alias.js +70 -0
- package/dist/cli/commands/alias.js.map +1 -0
- package/dist/cli/commands/channel.d.ts +13 -0
- package/dist/cli/commands/channel.d.ts.map +1 -0
- package/dist/cli/commands/channel.js +680 -0
- package/dist/cli/commands/channel.js.map +1 -0
- package/dist/cli/commands/completion.d.ts +8 -0
- package/dist/cli/commands/completion.d.ts.map +1 -0
- package/dist/cli/commands/completion.js +87 -0
- package/dist/cli/commands/completion.js.map +1 -0
- package/dist/cli/commands/config.d.ts +12 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +242 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/crud.d.ts +64 -0
- package/dist/cli/commands/crud.d.ts.map +1 -0
- package/dist/cli/commands/crud.js +805 -0
- package/dist/cli/commands/crud.js.map +1 -0
- package/dist/cli/commands/dep.d.ts +16 -0
- package/dist/cli/commands/dep.d.ts.map +1 -0
- package/dist/cli/commands/dep.js +499 -0
- package/dist/cli/commands/dep.js.map +1 -0
- package/dist/cli/commands/document.d.ts +12 -0
- package/dist/cli/commands/document.d.ts.map +1 -0
- package/dist/cli/commands/document.js +1039 -0
- package/dist/cli/commands/document.js.map +1 -0
- package/dist/cli/commands/embeddings.d.ts +12 -0
- package/dist/cli/commands/embeddings.d.ts.map +1 -0
- package/dist/cli/commands/embeddings.js +273 -0
- package/dist/cli/commands/embeddings.js.map +1 -0
- package/dist/cli/commands/entity.d.ts +16 -0
- package/dist/cli/commands/entity.d.ts.map +1 -0
- package/dist/cli/commands/entity.js +522 -0
- package/dist/cli/commands/entity.js.map +1 -0
- package/dist/cli/commands/gc.d.ts +10 -0
- package/dist/cli/commands/gc.d.ts.map +1 -0
- package/dist/cli/commands/gc.js +257 -0
- package/dist/cli/commands/gc.js.map +1 -0
- package/dist/cli/commands/help.d.ts +11 -0
- package/dist/cli/commands/help.d.ts.map +1 -0
- package/dist/cli/commands/help.js +169 -0
- package/dist/cli/commands/help.js.map +1 -0
- package/dist/cli/commands/history.d.ts +9 -0
- package/dist/cli/commands/history.d.ts.map +1 -0
- package/dist/cli/commands/history.js +160 -0
- package/dist/cli/commands/history.js.map +1 -0
- package/dist/cli/commands/identity.d.ts +18 -0
- package/dist/cli/commands/identity.d.ts.map +1 -0
- package/dist/cli/commands/identity.js +698 -0
- package/dist/cli/commands/identity.js.map +1 -0
- package/dist/cli/commands/inbox.d.ts +20 -0
- package/dist/cli/commands/inbox.d.ts.map +1 -0
- package/dist/cli/commands/inbox.js +493 -0
- package/dist/cli/commands/inbox.js.map +1 -0
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +144 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/install.d.ts +9 -0
- package/dist/cli/commands/install.d.ts.map +1 -0
- package/dist/cli/commands/install.js +200 -0
- package/dist/cli/commands/install.js.map +1 -0
- package/dist/cli/commands/library.d.ts +12 -0
- package/dist/cli/commands/library.d.ts.map +1 -0
- package/dist/cli/commands/library.js +665 -0
- package/dist/cli/commands/library.js.map +1 -0
- package/dist/cli/commands/message.d.ts +11 -0
- package/dist/cli/commands/message.d.ts.map +1 -0
- package/dist/cli/commands/message.js +608 -0
- package/dist/cli/commands/message.js.map +1 -0
- package/dist/cli/commands/plan.d.ts +17 -0
- package/dist/cli/commands/plan.d.ts.map +1 -0
- package/dist/cli/commands/plan.js +698 -0
- package/dist/cli/commands/plan.js.map +1 -0
- package/dist/cli/commands/playbook.d.ts +12 -0
- package/dist/cli/commands/playbook.d.ts.map +1 -0
- package/dist/cli/commands/playbook.js +730 -0
- package/dist/cli/commands/playbook.js.map +1 -0
- package/dist/cli/commands/reset.d.ts +12 -0
- package/dist/cli/commands/reset.d.ts.map +1 -0
- package/dist/cli/commands/reset.js +306 -0
- package/dist/cli/commands/reset.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +11 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +106 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +8 -0
- package/dist/cli/commands/stats.d.ts.map +1 -0
- package/dist/cli/commands/stats.js +82 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +14 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +370 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/task.d.ts +25 -0
- package/dist/cli/commands/task.d.ts.map +1 -0
- package/dist/cli/commands/task.js +1153 -0
- package/dist/cli/commands/task.js.map +1 -0
- package/dist/cli/commands/team.d.ts +13 -0
- package/dist/cli/commands/team.d.ts.map +1 -0
- package/dist/cli/commands/team.js +471 -0
- package/dist/cli/commands/team.js.map +1 -0
- package/dist/cli/commands/workflow.d.ts +16 -0
- package/dist/cli/commands/workflow.d.ts.map +1 -0
- package/dist/cli/commands/workflow.js +753 -0
- package/dist/cli/commands/workflow.js.map +1 -0
- package/dist/cli/completion.d.ts +28 -0
- package/dist/cli/completion.d.ts.map +1 -0
- package/dist/cli/completion.js +295 -0
- package/dist/cli/completion.js.map +1 -0
- package/dist/cli/db.d.ts +38 -0
- package/dist/cli/db.d.ts.map +1 -0
- package/dist/cli/db.js +90 -0
- package/dist/cli/db.js.map +1 -0
- package/dist/cli/formatter.d.ts +87 -0
- package/dist/cli/formatter.d.ts.map +1 -0
- package/dist/cli/formatter.js +464 -0
- package/dist/cli/formatter.js.map +1 -0
- package/dist/cli/index.d.ts +33 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +38 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/parser.d.ts +45 -0
- package/dist/cli/parser.d.ts.map +1 -0
- package/dist/cli/parser.js +256 -0
- package/dist/cli/parser.js.map +1 -0
- package/dist/cli/plugin-loader.d.ts +39 -0
- package/dist/cli/plugin-loader.d.ts.map +1 -0
- package/dist/cli/plugin-loader.js +165 -0
- package/dist/cli/plugin-loader.js.map +1 -0
- package/dist/cli/plugin-registry.d.ts +50 -0
- package/dist/cli/plugin-registry.d.ts.map +1 -0
- package/dist/cli/plugin-registry.js +206 -0
- package/dist/cli/plugin-registry.js.map +1 -0
- package/dist/cli/plugin-types.d.ts +106 -0
- package/dist/cli/plugin-types.d.ts.map +1 -0
- package/dist/cli/plugin-types.js +103 -0
- package/dist/cli/plugin-types.js.map +1 -0
- package/dist/cli/runner.d.ts +35 -0
- package/dist/cli/runner.d.ts.map +1 -0
- package/dist/cli/runner.js +340 -0
- package/dist/cli/runner.js.map +1 -0
- package/dist/cli/suggest.d.ts +15 -0
- package/dist/cli/suggest.d.ts.map +1 -0
- package/dist/cli/suggest.js +49 -0
- package/dist/cli/suggest.js.map +1 -0
- package/dist/cli/types.d.ts +138 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +63 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/config/config.d.ts +86 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +348 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/defaults.d.ts +66 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +114 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/duration.d.ts +75 -0
- package/dist/config/duration.d.ts.map +1 -0
- package/dist/config/duration.js +190 -0
- package/dist/config/duration.js.map +1 -0
- package/dist/config/env.d.ts +67 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +207 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/file.d.ts +97 -0
- package/dist/config/file.d.ts.map +1 -0
- package/dist/config/file.js +365 -0
- package/dist/config/file.js.map +1 -0
- package/dist/config/index.d.ts +35 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +41 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/merge.d.ts +53 -0
- package/dist/config/merge.d.ts.map +1 -0
- package/dist/config/merge.js +226 -0
- package/dist/config/merge.js.map +1 -0
- package/dist/config/types.d.ts +257 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +72 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config/validation.d.ts +55 -0
- package/dist/config/validation.d.ts.map +1 -0
- package/dist/config/validation.js +251 -0
- package/dist/config/validation.js.map +1 -0
- package/dist/http/index.d.ts +8 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +12 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/sync-handlers.d.ts +162 -0
- package/dist/http/sync-handlers.d.ts.map +1 -0
- package/dist/http/sync-handlers.js +271 -0
- package/dist/http/sync-handlers.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.d.ts +34 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +3329 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/static.d.ts +18 -0
- package/dist/server/static.d.ts.map +1 -0
- package/dist/server/static.js +71 -0
- package/dist/server/static.js.map +1 -0
- package/dist/server/ws/broadcaster.d.ts +8 -0
- package/dist/server/ws/broadcaster.d.ts.map +1 -0
- package/dist/server/ws/broadcaster.js +7 -0
- package/dist/server/ws/broadcaster.js.map +1 -0
- package/dist/server/ws/handler.d.ts +55 -0
- package/dist/server/ws/handler.d.ts.map +1 -0
- package/dist/server/ws/handler.js +160 -0
- package/dist/server/ws/handler.js.map +1 -0
- package/dist/services/blocked-cache.d.ts +297 -0
- package/dist/services/blocked-cache.d.ts.map +1 -0
- package/dist/services/blocked-cache.js +755 -0
- package/dist/services/blocked-cache.js.map +1 -0
- package/dist/services/dependency.d.ts +205 -0
- package/dist/services/dependency.d.ts.map +1 -0
- package/dist/services/dependency.js +566 -0
- package/dist/services/dependency.js.map +1 -0
- package/dist/services/embeddings/fusion.d.ts +33 -0
- package/dist/services/embeddings/fusion.d.ts.map +1 -0
- package/dist/services/embeddings/fusion.js +34 -0
- package/dist/services/embeddings/fusion.js.map +1 -0
- package/dist/services/embeddings/index.d.ts +12 -0
- package/dist/services/embeddings/index.d.ts.map +1 -0
- package/dist/services/embeddings/index.js +10 -0
- package/dist/services/embeddings/index.js.map +1 -0
- package/dist/services/embeddings/local-provider.d.ts +31 -0
- package/dist/services/embeddings/local-provider.d.ts.map +1 -0
- package/dist/services/embeddings/local-provider.js +80 -0
- package/dist/services/embeddings/local-provider.js.map +1 -0
- package/dist/services/embeddings/service.d.ts +76 -0
- package/dist/services/embeddings/service.d.ts.map +1 -0
- package/dist/services/embeddings/service.js +153 -0
- package/dist/services/embeddings/service.js.map +1 -0
- package/dist/services/embeddings/types.d.ts +70 -0
- package/dist/services/embeddings/types.d.ts.map +1 -0
- package/dist/services/embeddings/types.js +8 -0
- package/dist/services/embeddings/types.js.map +1 -0
- package/dist/services/id-length-cache.d.ts +156 -0
- package/dist/services/id-length-cache.d.ts.map +1 -0
- package/dist/services/id-length-cache.js +197 -0
- package/dist/services/id-length-cache.js.map +1 -0
- package/dist/services/inbox.d.ts +147 -0
- package/dist/services/inbox.d.ts.map +1 -0
- package/dist/services/inbox.js +428 -0
- package/dist/services/inbox.js.map +1 -0
- package/dist/services/index.d.ts +10 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +10 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/priority-service.d.ts +145 -0
- package/dist/services/priority-service.d.ts.map +1 -0
- package/dist/services/priority-service.js +272 -0
- package/dist/services/priority-service.js.map +1 -0
- package/dist/services/search-utils.d.ts +47 -0
- package/dist/services/search-utils.d.ts.map +1 -0
- package/dist/services/search-utils.js +83 -0
- package/dist/services/search-utils.js.map +1 -0
- package/dist/sync/hash.d.ts +48 -0
- package/dist/sync/hash.d.ts.map +1 -0
- package/dist/sync/hash.js +136 -0
- package/dist/sync/hash.js.map +1 -0
- package/dist/sync/index.d.ts +11 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +16 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/merge.d.ts +80 -0
- package/dist/sync/merge.d.ts.map +1 -0
- package/dist/sync/merge.js +310 -0
- package/dist/sync/merge.js.map +1 -0
- package/dist/sync/serialization.d.ts +132 -0
- package/dist/sync/serialization.d.ts.map +1 -0
- package/dist/sync/serialization.js +306 -0
- package/dist/sync/serialization.js.map +1 -0
- package/dist/sync/service.d.ts +102 -0
- package/dist/sync/service.d.ts.map +1 -0
- package/dist/sync/service.js +493 -0
- package/dist/sync/service.js.map +1 -0
- package/dist/sync/types.d.ts +275 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +76 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/systems/identity.d.ts +479 -0
- package/dist/systems/identity.d.ts.map +1 -0
- package/dist/systems/identity.js +817 -0
- package/dist/systems/identity.js.map +1 -0
- package/dist/systems/index.d.ts +8 -0
- package/dist/systems/index.d.ts.map +1 -0
- package/dist/systems/index.js +29 -0
- package/dist/systems/index.js.map +1 -0
- package/package.json +121 -0
- package/web/assets/charts-vendor-D1YcbGux.js +55 -0
- package/web/assets/dnd-vendor-DmxE-_ZH.js +5 -0
- package/web/assets/editor-vendor-BxraAWts.js +279 -0
- package/web/assets/index-B77vv208.js +341 -0
- package/web/assets/index-CF_XnVLh.css +1 -0
- package/web/assets/router-vendor-BCKpRBrB.js +41 -0
- package/web/assets/ui-vendor-DUahGnbT.js +45 -0
- package/web/assets/utils-vendor-CfYKiENT.js +813 -0
- package/web/favicon.ico +0 -0
- package/web/index.html +23 -0
- package/web/logo.png +0 -0
|
@@ -0,0 +1,1153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Commands - Task-specific CLI operations
|
|
3
|
+
*
|
|
4
|
+
* Provides CLI commands for task management:
|
|
5
|
+
* - ready: List tasks ready for work
|
|
6
|
+
* - blocked: List blocked tasks with reasons
|
|
7
|
+
* - close: Close a task
|
|
8
|
+
* - reopen: Reopen a closed task
|
|
9
|
+
* - assign: Assign a task to an entity
|
|
10
|
+
* - defer: Defer a task
|
|
11
|
+
* - undefer: Remove deferral from a task
|
|
12
|
+
*/
|
|
13
|
+
import { success, failure, ExitCode } from '../types.js';
|
|
14
|
+
import { getFormatter, getOutputMode } from '../formatter.js';
|
|
15
|
+
import { TaskStatus, TaskTypeValue, updateTaskStatus, isValidStatusTransition, createDocument, ContentType, } from '@stoneforge/core';
|
|
16
|
+
import { existsSync as fileExists, readFileSync } from 'node:fs';
|
|
17
|
+
import { resolve } from 'node:path';
|
|
18
|
+
import { createHandler, createOptions, listHandler, listOptions, showHandler, showOptions, updateHandler, updateOptions, deleteHandler, deleteOptions, } from './crud.js';
|
|
19
|
+
import { suggestCommands } from '../suggest.js';
|
|
20
|
+
import { createAPI } from '../db.js';
|
|
21
|
+
const readyOptions = [
|
|
22
|
+
{
|
|
23
|
+
name: 'assignee',
|
|
24
|
+
short: 'a',
|
|
25
|
+
description: 'Filter by assignee',
|
|
26
|
+
hasValue: true,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'priority',
|
|
30
|
+
short: 'p',
|
|
31
|
+
description: 'Filter by priority (1-5)',
|
|
32
|
+
hasValue: true,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'type',
|
|
36
|
+
short: 't',
|
|
37
|
+
description: 'Filter by task type (bug, feature, task, chore)',
|
|
38
|
+
hasValue: true,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'limit',
|
|
42
|
+
short: 'l',
|
|
43
|
+
description: 'Maximum number of results',
|
|
44
|
+
hasValue: true,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
async function readyHandler(_args, options) {
|
|
48
|
+
const { api, error } = createAPI(options);
|
|
49
|
+
if (error) {
|
|
50
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
// Build filter from options
|
|
54
|
+
const filter = {};
|
|
55
|
+
if (options.assignee) {
|
|
56
|
+
filter.assignee = options.assignee;
|
|
57
|
+
}
|
|
58
|
+
if (options.priority) {
|
|
59
|
+
const priority = parseInt(options.priority, 10);
|
|
60
|
+
if (isNaN(priority) || priority < 1 || priority > 5) {
|
|
61
|
+
return failure('Priority must be a number from 1 to 5', ExitCode.VALIDATION);
|
|
62
|
+
}
|
|
63
|
+
filter.priority = priority;
|
|
64
|
+
}
|
|
65
|
+
if (options.type) {
|
|
66
|
+
const validTypes = Object.values(TaskTypeValue);
|
|
67
|
+
if (!validTypes.includes(options.type)) {
|
|
68
|
+
return failure(`Invalid task type: ${options.type}. Must be one of: ${validTypes.join(', ')}`, ExitCode.VALIDATION);
|
|
69
|
+
}
|
|
70
|
+
filter.taskType = options.type;
|
|
71
|
+
}
|
|
72
|
+
if (options.limit) {
|
|
73
|
+
const limit = parseInt(options.limit, 10);
|
|
74
|
+
if (isNaN(limit) || limit < 1) {
|
|
75
|
+
return failure('Limit must be a positive number', ExitCode.VALIDATION);
|
|
76
|
+
}
|
|
77
|
+
filter.limit = limit;
|
|
78
|
+
}
|
|
79
|
+
// Get ready tasks
|
|
80
|
+
const tasks = await api.ready(filter);
|
|
81
|
+
// Format output based on mode
|
|
82
|
+
const mode = getOutputMode(options);
|
|
83
|
+
const formatter = getFormatter(mode);
|
|
84
|
+
if (mode === 'json') {
|
|
85
|
+
return success(tasks);
|
|
86
|
+
}
|
|
87
|
+
if (mode === 'quiet') {
|
|
88
|
+
return success(tasks.map((t) => t.id).join('\n'));
|
|
89
|
+
}
|
|
90
|
+
// Human-readable output
|
|
91
|
+
if (tasks.length === 0) {
|
|
92
|
+
return success(null, 'No ready tasks found');
|
|
93
|
+
}
|
|
94
|
+
// Build table data
|
|
95
|
+
const headers = ['ID', 'TITLE', 'PRIORITY', 'ASSIGNEE', 'TYPE'];
|
|
96
|
+
const rows = tasks.map((task) => [
|
|
97
|
+
task.id,
|
|
98
|
+
task.title.length > 40 ? task.title.substring(0, 37) + '...' : task.title,
|
|
99
|
+
`P${task.priority}`,
|
|
100
|
+
task.assignee ?? '-',
|
|
101
|
+
task.taskType,
|
|
102
|
+
]);
|
|
103
|
+
const table = formatter.table(headers, rows);
|
|
104
|
+
const summary = `\n${tasks.length} ready task(s)`;
|
|
105
|
+
return success(tasks, table + summary);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
109
|
+
return failure(`Failed to get ready tasks: ${message}`, ExitCode.GENERAL_ERROR);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export const readyCommand = {
|
|
113
|
+
name: 'ready',
|
|
114
|
+
description: 'List tasks ready for work',
|
|
115
|
+
usage: 'sf ready [options]',
|
|
116
|
+
help: `List tasks that are ready for work.
|
|
117
|
+
|
|
118
|
+
Ready tasks are:
|
|
119
|
+
- Status is 'open' or 'in_progress'
|
|
120
|
+
- Not blocked by any dependency
|
|
121
|
+
- scheduledFor is null or in the past
|
|
122
|
+
|
|
123
|
+
Options:
|
|
124
|
+
-a, --assignee <id> Filter by assignee entity ID
|
|
125
|
+
-p, --priority <1-5> Filter by priority
|
|
126
|
+
-t, --type <type> Filter by task type (bug, feature, task, chore)
|
|
127
|
+
-l, --limit <n> Maximum number of results
|
|
128
|
+
|
|
129
|
+
Examples:
|
|
130
|
+
sf ready
|
|
131
|
+
sf ready --assignee alice
|
|
132
|
+
sf ready --priority 1
|
|
133
|
+
sf ready -a alice -p 1 -l 10`,
|
|
134
|
+
options: readyOptions,
|
|
135
|
+
handler: readyHandler,
|
|
136
|
+
};
|
|
137
|
+
const backlogOptions = [
|
|
138
|
+
{
|
|
139
|
+
name: 'priority',
|
|
140
|
+
short: 'p',
|
|
141
|
+
description: 'Filter by priority (1-5)',
|
|
142
|
+
hasValue: true,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'limit',
|
|
146
|
+
short: 'l',
|
|
147
|
+
description: 'Maximum number of results',
|
|
148
|
+
hasValue: true,
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
async function backlogHandler(_args, options) {
|
|
152
|
+
const { api, error } = createAPI(options);
|
|
153
|
+
if (error) {
|
|
154
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const filter = {};
|
|
158
|
+
if (options.priority) {
|
|
159
|
+
const priority = parseInt(options.priority, 10);
|
|
160
|
+
if (isNaN(priority) || priority < 1 || priority > 5) {
|
|
161
|
+
return failure('Priority must be a number from 1 to 5', ExitCode.VALIDATION);
|
|
162
|
+
}
|
|
163
|
+
filter.priority = priority;
|
|
164
|
+
}
|
|
165
|
+
if (options.limit) {
|
|
166
|
+
const limit = parseInt(options.limit, 10);
|
|
167
|
+
if (isNaN(limit) || limit < 1) {
|
|
168
|
+
return failure('Limit must be a positive number', ExitCode.VALIDATION);
|
|
169
|
+
}
|
|
170
|
+
filter.limit = limit;
|
|
171
|
+
}
|
|
172
|
+
const tasks = await api.backlog(filter);
|
|
173
|
+
const mode = getOutputMode(options);
|
|
174
|
+
const formatter = getFormatter(mode);
|
|
175
|
+
if (mode === 'json') {
|
|
176
|
+
return success(tasks);
|
|
177
|
+
}
|
|
178
|
+
if (mode === 'quiet') {
|
|
179
|
+
return success(tasks.map((t) => t.id).join('\n'));
|
|
180
|
+
}
|
|
181
|
+
if (tasks.length === 0) {
|
|
182
|
+
return success(null, 'No backlog tasks found');
|
|
183
|
+
}
|
|
184
|
+
const headers = ['ID', 'TITLE', 'PRIORITY', 'TYPE', 'CREATED'];
|
|
185
|
+
const rows = tasks.map((task) => [
|
|
186
|
+
task.id,
|
|
187
|
+
task.title.length > 40 ? task.title.substring(0, 37) + '...' : task.title,
|
|
188
|
+
`P${task.priority}`,
|
|
189
|
+
task.taskType,
|
|
190
|
+
new Date(task.createdAt).toLocaleDateString(),
|
|
191
|
+
]);
|
|
192
|
+
const table = formatter.table(headers, rows);
|
|
193
|
+
const summary = `\n${tasks.length} backlog task(s)`;
|
|
194
|
+
return success(tasks, table + summary);
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
198
|
+
return failure(`Failed to get backlog tasks: ${message}`, ExitCode.GENERAL_ERROR);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export const backlogCommand = {
|
|
202
|
+
name: 'backlog',
|
|
203
|
+
description: 'List tasks in backlog',
|
|
204
|
+
usage: 'sf backlog [options]',
|
|
205
|
+
help: `List tasks in backlog (not ready for work, needs triage).
|
|
206
|
+
|
|
207
|
+
Backlog tasks are excluded from ready() and won't be auto-dispatched.
|
|
208
|
+
|
|
209
|
+
Options:
|
|
210
|
+
-p, --priority <1-5> Filter by priority
|
|
211
|
+
-l, --limit <n> Maximum number of results
|
|
212
|
+
|
|
213
|
+
Examples:
|
|
214
|
+
sf backlog
|
|
215
|
+
sf backlog --priority 1
|
|
216
|
+
sf backlog -l 10`,
|
|
217
|
+
options: backlogOptions,
|
|
218
|
+
handler: backlogHandler,
|
|
219
|
+
};
|
|
220
|
+
const blockedOptions = [
|
|
221
|
+
{
|
|
222
|
+
name: 'assignee',
|
|
223
|
+
short: 'a',
|
|
224
|
+
description: 'Filter by assignee',
|
|
225
|
+
hasValue: true,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'priority',
|
|
229
|
+
short: 'p',
|
|
230
|
+
description: 'Filter by priority (1-5)',
|
|
231
|
+
hasValue: true,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: 'limit',
|
|
235
|
+
short: 'l',
|
|
236
|
+
description: 'Maximum number of results',
|
|
237
|
+
hasValue: true,
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
async function blockedHandler(_args, options) {
|
|
241
|
+
const { api, error } = createAPI(options);
|
|
242
|
+
if (error) {
|
|
243
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
// Build filter from options
|
|
247
|
+
const filter = {};
|
|
248
|
+
if (options.assignee) {
|
|
249
|
+
filter.assignee = options.assignee;
|
|
250
|
+
}
|
|
251
|
+
if (options.priority) {
|
|
252
|
+
const priority = parseInt(options.priority, 10);
|
|
253
|
+
if (isNaN(priority) || priority < 1 || priority > 5) {
|
|
254
|
+
return failure('Priority must be a number from 1 to 5', ExitCode.VALIDATION);
|
|
255
|
+
}
|
|
256
|
+
filter.priority = priority;
|
|
257
|
+
}
|
|
258
|
+
if (options.limit) {
|
|
259
|
+
const limit = parseInt(options.limit, 10);
|
|
260
|
+
if (isNaN(limit) || limit < 1) {
|
|
261
|
+
return failure('Limit must be a positive number', ExitCode.VALIDATION);
|
|
262
|
+
}
|
|
263
|
+
filter.limit = limit;
|
|
264
|
+
}
|
|
265
|
+
// Get blocked tasks
|
|
266
|
+
const tasks = await api.blocked(filter);
|
|
267
|
+
// Format output based on mode
|
|
268
|
+
const mode = getOutputMode(options);
|
|
269
|
+
const formatter = getFormatter(mode);
|
|
270
|
+
if (mode === 'json') {
|
|
271
|
+
return success(tasks);
|
|
272
|
+
}
|
|
273
|
+
if (mode === 'quiet') {
|
|
274
|
+
return success(tasks.map((t) => t.id).join('\n'));
|
|
275
|
+
}
|
|
276
|
+
// Human-readable output
|
|
277
|
+
if (tasks.length === 0) {
|
|
278
|
+
return success(null, 'No blocked tasks found');
|
|
279
|
+
}
|
|
280
|
+
// Build table data
|
|
281
|
+
const headers = ['ID', 'TITLE', 'BLOCKED BY', 'REASON'];
|
|
282
|
+
const rows = tasks.map((task) => [
|
|
283
|
+
task.id,
|
|
284
|
+
task.title.length > 30 ? task.title.substring(0, 27) + '...' : task.title,
|
|
285
|
+
task.blockedBy,
|
|
286
|
+
task.blockReason.length > 30 ? task.blockReason.substring(0, 27) + '...' : task.blockReason,
|
|
287
|
+
]);
|
|
288
|
+
const table = formatter.table(headers, rows);
|
|
289
|
+
const summary = `\n${tasks.length} blocked task(s)`;
|
|
290
|
+
return success(tasks, table + summary);
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
294
|
+
return failure(`Failed to get blocked tasks: ${message}`, ExitCode.GENERAL_ERROR);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
export const blockedCommand = {
|
|
298
|
+
name: 'blocked',
|
|
299
|
+
description: 'List blocked tasks with reasons',
|
|
300
|
+
usage: 'sf blocked [options]',
|
|
301
|
+
help: `List tasks that are blocked with blocking details.
|
|
302
|
+
|
|
303
|
+
Options:
|
|
304
|
+
-a, --assignee <id> Filter by assignee entity ID
|
|
305
|
+
-p, --priority <1-5> Filter by priority
|
|
306
|
+
-l, --limit <n> Maximum number of results
|
|
307
|
+
|
|
308
|
+
Examples:
|
|
309
|
+
sf blocked
|
|
310
|
+
sf blocked --assignee alice
|
|
311
|
+
sf blocked --json`,
|
|
312
|
+
options: blockedOptions,
|
|
313
|
+
handler: blockedHandler,
|
|
314
|
+
};
|
|
315
|
+
const closeOptions = [
|
|
316
|
+
{
|
|
317
|
+
name: 'reason',
|
|
318
|
+
short: 'r',
|
|
319
|
+
description: 'Close reason',
|
|
320
|
+
hasValue: true,
|
|
321
|
+
},
|
|
322
|
+
];
|
|
323
|
+
async function closeHandler(args, options) {
|
|
324
|
+
const [id] = args;
|
|
325
|
+
if (!id) {
|
|
326
|
+
return failure('Usage: sf task close <id> [--reason "reason"]', ExitCode.INVALID_ARGUMENTS);
|
|
327
|
+
}
|
|
328
|
+
const { api, error } = createAPI(options);
|
|
329
|
+
if (error) {
|
|
330
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
// Get the task
|
|
334
|
+
const task = await api.get(id);
|
|
335
|
+
if (!task) {
|
|
336
|
+
return failure(`Task not found: ${id}`, ExitCode.NOT_FOUND);
|
|
337
|
+
}
|
|
338
|
+
if (task.type !== 'task') {
|
|
339
|
+
return failure(`Element is not a task: ${id}`, ExitCode.VALIDATION);
|
|
340
|
+
}
|
|
341
|
+
// Check if already closed
|
|
342
|
+
if (task.status === TaskStatus.CLOSED) {
|
|
343
|
+
return failure(`Task is already closed: ${id}`, ExitCode.VALIDATION);
|
|
344
|
+
}
|
|
345
|
+
// Check if transition is valid
|
|
346
|
+
if (!isValidStatusTransition(task.status, TaskStatus.CLOSED)) {
|
|
347
|
+
return failure(`Cannot close task with status '${task.status}'`, ExitCode.VALIDATION);
|
|
348
|
+
}
|
|
349
|
+
// Update the task
|
|
350
|
+
const updated = updateTaskStatus(task, {
|
|
351
|
+
status: TaskStatus.CLOSED,
|
|
352
|
+
closeReason: options.reason,
|
|
353
|
+
});
|
|
354
|
+
// Save the update with optimistic concurrency control
|
|
355
|
+
await api.update(id, updated, {
|
|
356
|
+
expectedUpdatedAt: task.updatedAt,
|
|
357
|
+
});
|
|
358
|
+
// Format output based on mode
|
|
359
|
+
const mode = getOutputMode(options);
|
|
360
|
+
if (mode === 'json') {
|
|
361
|
+
return success(updated);
|
|
362
|
+
}
|
|
363
|
+
if (mode === 'quiet') {
|
|
364
|
+
return success(updated.id);
|
|
365
|
+
}
|
|
366
|
+
return success(updated, `Closed task ${id}`);
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
370
|
+
return failure(`Failed to close task: ${message}`, ExitCode.GENERAL_ERROR);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
export const closeCommand = {
|
|
374
|
+
name: 'close',
|
|
375
|
+
description: 'Close a task',
|
|
376
|
+
usage: 'sf close <id> [options]',
|
|
377
|
+
help: `Close a task, marking it as completed.
|
|
378
|
+
|
|
379
|
+
Arguments:
|
|
380
|
+
id Task identifier (e.g., el-abc123)
|
|
381
|
+
|
|
382
|
+
Options:
|
|
383
|
+
-r, --reason <text> Close reason
|
|
384
|
+
|
|
385
|
+
Examples:
|
|
386
|
+
sf close el-abc123
|
|
387
|
+
sf close el-abc123 --reason "Fixed in PR #42"`,
|
|
388
|
+
options: closeOptions,
|
|
389
|
+
handler: closeHandler,
|
|
390
|
+
};
|
|
391
|
+
const reopenOptions = [
|
|
392
|
+
{
|
|
393
|
+
name: 'message',
|
|
394
|
+
short: 'm',
|
|
395
|
+
description: 'Message to append to the task description explaining why it was reopened',
|
|
396
|
+
hasValue: true,
|
|
397
|
+
},
|
|
398
|
+
];
|
|
399
|
+
async function reopenHandler(args, options) {
|
|
400
|
+
const [id] = args;
|
|
401
|
+
if (!id) {
|
|
402
|
+
return failure('Usage: sf task reopen <id> [--message "reason"]', ExitCode.INVALID_ARGUMENTS);
|
|
403
|
+
}
|
|
404
|
+
const { api, error } = createAPI(options);
|
|
405
|
+
if (error) {
|
|
406
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
// Get the task
|
|
410
|
+
const task = await api.get(id);
|
|
411
|
+
if (!task) {
|
|
412
|
+
return failure(`Task not found: ${id}`, ExitCode.NOT_FOUND);
|
|
413
|
+
}
|
|
414
|
+
if (task.type !== 'task') {
|
|
415
|
+
return failure(`Element is not a task: ${id}`, ExitCode.VALIDATION);
|
|
416
|
+
}
|
|
417
|
+
// Check if not closed
|
|
418
|
+
if (task.status !== TaskStatus.CLOSED) {
|
|
419
|
+
return failure(`Task is not closed (status: ${task.status})`, ExitCode.VALIDATION);
|
|
420
|
+
}
|
|
421
|
+
// Update status to OPEN (clears closedAt)
|
|
422
|
+
const updated = updateTaskStatus(task, {
|
|
423
|
+
status: TaskStatus.OPEN,
|
|
424
|
+
});
|
|
425
|
+
// Clear assignee and closeReason
|
|
426
|
+
updated.assignee = undefined;
|
|
427
|
+
updated.closeReason = undefined;
|
|
428
|
+
// Clear orchestrator metadata fields while preserving branch/worktree/handoff info
|
|
429
|
+
const orchestratorMeta = updated.metadata?.orchestrator;
|
|
430
|
+
if (orchestratorMeta) {
|
|
431
|
+
const reconciliationCount = orchestratorMeta.reconciliationCount ?? 0;
|
|
432
|
+
const clearedMeta = {
|
|
433
|
+
...orchestratorMeta,
|
|
434
|
+
mergeStatus: undefined,
|
|
435
|
+
mergedAt: undefined,
|
|
436
|
+
mergeFailureReason: undefined,
|
|
437
|
+
assignedAgent: undefined,
|
|
438
|
+
sessionId: undefined,
|
|
439
|
+
startedAt: undefined,
|
|
440
|
+
completedAt: undefined,
|
|
441
|
+
completionSummary: undefined,
|
|
442
|
+
lastCommitHash: undefined,
|
|
443
|
+
testRunCount: undefined,
|
|
444
|
+
lastTestResult: undefined,
|
|
445
|
+
lastSyncResult: undefined,
|
|
446
|
+
reconciliationCount: reconciliationCount + 1,
|
|
447
|
+
};
|
|
448
|
+
updated.metadata = {
|
|
449
|
+
...updated.metadata,
|
|
450
|
+
orchestrator: clearedMeta,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
// Save the update with optimistic concurrency control
|
|
454
|
+
await api.update(id, updated, {
|
|
455
|
+
expectedUpdatedAt: task.updatedAt,
|
|
456
|
+
});
|
|
457
|
+
// If message provided, append to or create description document
|
|
458
|
+
if (options.message) {
|
|
459
|
+
const reopenLine = `**Re-opened** — Task was closed but incomplete. Message: ${options.message}`;
|
|
460
|
+
if (task.descriptionRef) {
|
|
461
|
+
try {
|
|
462
|
+
const doc = await api.get(task.descriptionRef);
|
|
463
|
+
if (doc) {
|
|
464
|
+
await api.update(task.descriptionRef, {
|
|
465
|
+
content: doc.content + '\n\n' + reopenLine,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
// Non-fatal: message is still shown in output
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
const actor = options.actor;
|
|
475
|
+
const newDoc = await createDocument({
|
|
476
|
+
content: reopenLine,
|
|
477
|
+
contentType: ContentType.MARKDOWN,
|
|
478
|
+
createdBy: actor ?? 'operator',
|
|
479
|
+
});
|
|
480
|
+
const created = await api.create(newDoc);
|
|
481
|
+
await api.update(id, { descriptionRef: created.id }, { actor });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Re-fetch task to get latest state (including any descriptionRef changes)
|
|
485
|
+
const finalTask = await api.get(id);
|
|
486
|
+
// Format output based on mode
|
|
487
|
+
const mode = getOutputMode(options);
|
|
488
|
+
if (mode === 'json') {
|
|
489
|
+
return success(finalTask ?? updated);
|
|
490
|
+
}
|
|
491
|
+
if (mode === 'quiet') {
|
|
492
|
+
return success(updated.id);
|
|
493
|
+
}
|
|
494
|
+
return success(finalTask ?? updated, `Reopened task ${id}`);
|
|
495
|
+
}
|
|
496
|
+
catch (err) {
|
|
497
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
498
|
+
return failure(`Failed to reopen task: ${message}`, ExitCode.GENERAL_ERROR);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
export const reopenCommand = {
|
|
502
|
+
name: 'reopen',
|
|
503
|
+
description: 'Reopen a closed task',
|
|
504
|
+
usage: 'sf reopen <id> [--message "reason"]',
|
|
505
|
+
help: `Reopen a previously closed task, clearing assignment and merge metadata.
|
|
506
|
+
|
|
507
|
+
Arguments:
|
|
508
|
+
id Task identifier (e.g., el-abc123)
|
|
509
|
+
|
|
510
|
+
Options:
|
|
511
|
+
-m, --message Message to append to the task description
|
|
512
|
+
|
|
513
|
+
Examples:
|
|
514
|
+
sf reopen el-abc123
|
|
515
|
+
sf reopen el-abc123 --message "Work was incomplete, needs fixes"`,
|
|
516
|
+
options: reopenOptions,
|
|
517
|
+
handler: reopenHandler,
|
|
518
|
+
};
|
|
519
|
+
const assignOptions = [
|
|
520
|
+
{
|
|
521
|
+
name: 'unassign',
|
|
522
|
+
short: 'u',
|
|
523
|
+
description: 'Remove assignment',
|
|
524
|
+
},
|
|
525
|
+
];
|
|
526
|
+
async function assignHandler(args, options) {
|
|
527
|
+
const [id, assignee] = args;
|
|
528
|
+
if (!id) {
|
|
529
|
+
return failure('Usage: sf task assign <id> [assignee] [--unassign]', ExitCode.INVALID_ARGUMENTS);
|
|
530
|
+
}
|
|
531
|
+
if (!assignee && !options.unassign) {
|
|
532
|
+
return failure('Specify an assignee or use --unassign to remove assignment', ExitCode.INVALID_ARGUMENTS);
|
|
533
|
+
}
|
|
534
|
+
const { api, error } = createAPI(options);
|
|
535
|
+
if (error) {
|
|
536
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
// Get the task
|
|
540
|
+
const task = await api.get(id);
|
|
541
|
+
if (!task) {
|
|
542
|
+
return failure(`Task not found: ${id}`, ExitCode.NOT_FOUND);
|
|
543
|
+
}
|
|
544
|
+
if (task.type !== 'task') {
|
|
545
|
+
return failure(`Element is not a task: ${id}`, ExitCode.VALIDATION);
|
|
546
|
+
}
|
|
547
|
+
// Update assignment
|
|
548
|
+
const updates = {
|
|
549
|
+
assignee: options.unassign ? undefined : assignee,
|
|
550
|
+
};
|
|
551
|
+
// Save the update with optimistic concurrency control
|
|
552
|
+
const updated = await api.update(id, updates, {
|
|
553
|
+
expectedUpdatedAt: task.updatedAt,
|
|
554
|
+
});
|
|
555
|
+
// Format output based on mode
|
|
556
|
+
const mode = getOutputMode(options);
|
|
557
|
+
if (mode === 'json') {
|
|
558
|
+
return success(updated);
|
|
559
|
+
}
|
|
560
|
+
if (mode === 'quiet') {
|
|
561
|
+
return success(updated.id);
|
|
562
|
+
}
|
|
563
|
+
const message = options.unassign
|
|
564
|
+
? `Unassigned task ${id}`
|
|
565
|
+
: `Assigned task ${id} to ${assignee}`;
|
|
566
|
+
return success(updated, message);
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
570
|
+
return failure(`Failed to assign task: ${message}`, ExitCode.GENERAL_ERROR);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
export const assignCommand = {
|
|
574
|
+
name: 'assign',
|
|
575
|
+
description: 'Assign a task to an entity',
|
|
576
|
+
usage: 'sf assign <id> [assignee]',
|
|
577
|
+
help: `Assign a task to an entity.
|
|
578
|
+
|
|
579
|
+
Arguments:
|
|
580
|
+
id Task identifier (e.g., el-abc123)
|
|
581
|
+
assignee Entity to assign to
|
|
582
|
+
|
|
583
|
+
Options:
|
|
584
|
+
-u, --unassign Remove assignment
|
|
585
|
+
|
|
586
|
+
Examples:
|
|
587
|
+
sf assign el-abc123 alice
|
|
588
|
+
sf assign el-abc123 --unassign`,
|
|
589
|
+
options: assignOptions,
|
|
590
|
+
handler: assignHandler,
|
|
591
|
+
};
|
|
592
|
+
const deferOptions = [
|
|
593
|
+
{
|
|
594
|
+
name: 'until',
|
|
595
|
+
description: 'Schedule for date (ISO format)',
|
|
596
|
+
hasValue: true,
|
|
597
|
+
},
|
|
598
|
+
];
|
|
599
|
+
async function deferHandler(args, options) {
|
|
600
|
+
const [id] = args;
|
|
601
|
+
if (!id) {
|
|
602
|
+
return failure('Usage: sf task defer <id> [--until date]', ExitCode.INVALID_ARGUMENTS);
|
|
603
|
+
}
|
|
604
|
+
const { api, error } = createAPI(options);
|
|
605
|
+
if (error) {
|
|
606
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
607
|
+
}
|
|
608
|
+
try {
|
|
609
|
+
// Get the task
|
|
610
|
+
const task = await api.get(id);
|
|
611
|
+
if (!task) {
|
|
612
|
+
return failure(`Task not found: ${id}`, ExitCode.NOT_FOUND);
|
|
613
|
+
}
|
|
614
|
+
if (task.type !== 'task') {
|
|
615
|
+
return failure(`Element is not a task: ${id}`, ExitCode.VALIDATION);
|
|
616
|
+
}
|
|
617
|
+
// Check if transition is valid
|
|
618
|
+
if (!isValidStatusTransition(task.status, TaskStatus.DEFERRED)) {
|
|
619
|
+
return failure(`Cannot defer task with status '${task.status}'`, ExitCode.VALIDATION);
|
|
620
|
+
}
|
|
621
|
+
// Parse until date if provided
|
|
622
|
+
let scheduledFor;
|
|
623
|
+
if (options.until) {
|
|
624
|
+
const date = new Date(options.until);
|
|
625
|
+
if (isNaN(date.getTime())) {
|
|
626
|
+
return failure(`Invalid date format: ${options.until}`, ExitCode.VALIDATION);
|
|
627
|
+
}
|
|
628
|
+
scheduledFor = date.toISOString();
|
|
629
|
+
}
|
|
630
|
+
// Update the task
|
|
631
|
+
const updated = updateTaskStatus(task, {
|
|
632
|
+
status: TaskStatus.DEFERRED,
|
|
633
|
+
});
|
|
634
|
+
// Add scheduledFor if provided
|
|
635
|
+
if (scheduledFor) {
|
|
636
|
+
updated.scheduledFor = scheduledFor;
|
|
637
|
+
}
|
|
638
|
+
// Save the update with optimistic concurrency control
|
|
639
|
+
await api.update(id, updated, {
|
|
640
|
+
expectedUpdatedAt: task.updatedAt,
|
|
641
|
+
});
|
|
642
|
+
// Format output based on mode
|
|
643
|
+
const mode = getOutputMode(options);
|
|
644
|
+
if (mode === 'json') {
|
|
645
|
+
return success(updated);
|
|
646
|
+
}
|
|
647
|
+
if (mode === 'quiet') {
|
|
648
|
+
return success(updated.id);
|
|
649
|
+
}
|
|
650
|
+
const message = scheduledFor
|
|
651
|
+
? `Deferred task ${id} until ${new Date(scheduledFor).toLocaleDateString()}`
|
|
652
|
+
: `Deferred task ${id}`;
|
|
653
|
+
return success(updated, message);
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
657
|
+
return failure(`Failed to defer task: ${message}`, ExitCode.GENERAL_ERROR);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
export const deferCommand = {
|
|
661
|
+
name: 'defer',
|
|
662
|
+
description: 'Defer a task',
|
|
663
|
+
usage: 'sf defer <id> [options]',
|
|
664
|
+
help: `Defer a task, putting it on hold.
|
|
665
|
+
|
|
666
|
+
Arguments:
|
|
667
|
+
id Task identifier (e.g., el-abc123)
|
|
668
|
+
|
|
669
|
+
Options:
|
|
670
|
+
--until <date> Schedule for a specific date (ISO format)
|
|
671
|
+
|
|
672
|
+
Examples:
|
|
673
|
+
sf defer el-abc123
|
|
674
|
+
sf defer el-abc123 --until 2024-03-01`,
|
|
675
|
+
options: deferOptions,
|
|
676
|
+
handler: deferHandler,
|
|
677
|
+
};
|
|
678
|
+
// ============================================================================
|
|
679
|
+
// Undefer Command
|
|
680
|
+
// ============================================================================
|
|
681
|
+
async function undeferHandler(args, options) {
|
|
682
|
+
const [id] = args;
|
|
683
|
+
if (!id) {
|
|
684
|
+
return failure('Usage: sf task undefer <id>', ExitCode.INVALID_ARGUMENTS);
|
|
685
|
+
}
|
|
686
|
+
const { api, error } = createAPI(options);
|
|
687
|
+
if (error) {
|
|
688
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
// Get the task
|
|
692
|
+
const task = await api.get(id);
|
|
693
|
+
if (!task) {
|
|
694
|
+
return failure(`Task not found: ${id}`, ExitCode.NOT_FOUND);
|
|
695
|
+
}
|
|
696
|
+
if (task.type !== 'task') {
|
|
697
|
+
return failure(`Element is not a task: ${id}`, ExitCode.VALIDATION);
|
|
698
|
+
}
|
|
699
|
+
// Check if deferred
|
|
700
|
+
if (task.status !== TaskStatus.DEFERRED) {
|
|
701
|
+
return failure(`Task is not deferred (status: ${task.status})`, ExitCode.VALIDATION);
|
|
702
|
+
}
|
|
703
|
+
// Update the task - reopen it
|
|
704
|
+
const updated = updateTaskStatus(task, {
|
|
705
|
+
status: TaskStatus.OPEN,
|
|
706
|
+
});
|
|
707
|
+
// Clear scheduledFor
|
|
708
|
+
updated.scheduledFor = undefined;
|
|
709
|
+
// Save the update with optimistic concurrency control
|
|
710
|
+
await api.update(id, updated, {
|
|
711
|
+
expectedUpdatedAt: task.updatedAt,
|
|
712
|
+
});
|
|
713
|
+
// Format output based on mode
|
|
714
|
+
const mode = getOutputMode(options);
|
|
715
|
+
if (mode === 'json') {
|
|
716
|
+
return success(updated);
|
|
717
|
+
}
|
|
718
|
+
if (mode === 'quiet') {
|
|
719
|
+
return success(updated.id);
|
|
720
|
+
}
|
|
721
|
+
return success(updated, `Undeferred task ${id}`);
|
|
722
|
+
}
|
|
723
|
+
catch (err) {
|
|
724
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
725
|
+
return failure(`Failed to undefer task: ${message}`, ExitCode.GENERAL_ERROR);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
export const undeferCommand = {
|
|
729
|
+
name: 'undefer',
|
|
730
|
+
description: 'Remove deferral from a task',
|
|
731
|
+
usage: 'sf undefer <id>',
|
|
732
|
+
help: `Remove deferral from a task, making it ready for work again.
|
|
733
|
+
|
|
734
|
+
Arguments:
|
|
735
|
+
id Task identifier (e.g., el-abc123)
|
|
736
|
+
|
|
737
|
+
Examples:
|
|
738
|
+
sf undefer el-abc123`,
|
|
739
|
+
options: [],
|
|
740
|
+
handler: undeferHandler,
|
|
741
|
+
};
|
|
742
|
+
const describeOptions = [
|
|
743
|
+
{
|
|
744
|
+
name: 'content',
|
|
745
|
+
short: 'c',
|
|
746
|
+
description: 'Description content (text)',
|
|
747
|
+
hasValue: true,
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
name: 'file',
|
|
751
|
+
short: 'f',
|
|
752
|
+
description: 'Read description from file',
|
|
753
|
+
hasValue: true,
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
name: 'show',
|
|
757
|
+
short: 's',
|
|
758
|
+
description: 'Show current description instead of setting it',
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
name: 'append',
|
|
762
|
+
description: 'Append to existing description instead of replacing',
|
|
763
|
+
},
|
|
764
|
+
];
|
|
765
|
+
async function describeHandler(args, options) {
|
|
766
|
+
const [id] = args;
|
|
767
|
+
if (!id) {
|
|
768
|
+
return failure('Usage: sf task describe <id> --content <text> | --file <path> | --show', ExitCode.INVALID_ARGUMENTS);
|
|
769
|
+
}
|
|
770
|
+
const { api, error } = createAPI(options);
|
|
771
|
+
if (error) {
|
|
772
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
773
|
+
}
|
|
774
|
+
try {
|
|
775
|
+
// Get the task
|
|
776
|
+
const task = await api.get(id);
|
|
777
|
+
if (!task) {
|
|
778
|
+
return failure(`Task not found: ${id}`, ExitCode.NOT_FOUND);
|
|
779
|
+
}
|
|
780
|
+
if (task.type !== 'task') {
|
|
781
|
+
return failure(`Element is not a task: ${id}`, ExitCode.VALIDATION);
|
|
782
|
+
}
|
|
783
|
+
// Show mode - display current description
|
|
784
|
+
if (options.show) {
|
|
785
|
+
const mode = getOutputMode(options);
|
|
786
|
+
if (!task.descriptionRef) {
|
|
787
|
+
if (mode === 'json') {
|
|
788
|
+
return success({ taskId: id, description: null });
|
|
789
|
+
}
|
|
790
|
+
return success(null, `Task ${id} has no description`);
|
|
791
|
+
}
|
|
792
|
+
// Get the description document
|
|
793
|
+
const doc = await api.get(task.descriptionRef);
|
|
794
|
+
if (!doc) {
|
|
795
|
+
return failure(`Description document not found: ${task.descriptionRef}`, ExitCode.NOT_FOUND);
|
|
796
|
+
}
|
|
797
|
+
if (mode === 'json') {
|
|
798
|
+
return success({ taskId: id, descriptionRef: task.descriptionRef, content: doc.content });
|
|
799
|
+
}
|
|
800
|
+
if (mode === 'quiet') {
|
|
801
|
+
return success(doc.content);
|
|
802
|
+
}
|
|
803
|
+
return success(doc, `Description for task ${id}:\n\n${doc.content}`);
|
|
804
|
+
}
|
|
805
|
+
// Set mode - must specify either --content or --file
|
|
806
|
+
if (!options.content && !options.file) {
|
|
807
|
+
return failure('Either --content, --file, or --show is required', ExitCode.INVALID_ARGUMENTS);
|
|
808
|
+
}
|
|
809
|
+
if (options.content && options.file) {
|
|
810
|
+
return failure('Cannot specify both --content and --file', ExitCode.INVALID_ARGUMENTS);
|
|
811
|
+
}
|
|
812
|
+
// Get new content
|
|
813
|
+
let content;
|
|
814
|
+
if (options.content) {
|
|
815
|
+
content = options.content;
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
const filePath = resolve(options.file);
|
|
819
|
+
if (!fileExists(filePath)) {
|
|
820
|
+
return failure(`File not found: ${filePath}`, ExitCode.NOT_FOUND);
|
|
821
|
+
}
|
|
822
|
+
content = readFileSync(filePath, 'utf-8');
|
|
823
|
+
}
|
|
824
|
+
const actor = options.actor;
|
|
825
|
+
// Check if task already has a description document
|
|
826
|
+
if (task.descriptionRef) {
|
|
827
|
+
let finalContent = content;
|
|
828
|
+
// If appending, fetch existing content and combine
|
|
829
|
+
if (options.append) {
|
|
830
|
+
const existingDoc = await api.get(task.descriptionRef);
|
|
831
|
+
if (existingDoc) {
|
|
832
|
+
finalContent = existingDoc.content + '\n\n' + content;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
// Update existing document
|
|
836
|
+
const updated = await api.update(task.descriptionRef, { content: finalContent }, { actor });
|
|
837
|
+
const mode = getOutputMode(options);
|
|
838
|
+
if (mode === 'json') {
|
|
839
|
+
return success({ taskId: id, descriptionRef: task.descriptionRef, document: updated, appended: options.append ?? false });
|
|
840
|
+
}
|
|
841
|
+
if (mode === 'quiet') {
|
|
842
|
+
return success(task.descriptionRef);
|
|
843
|
+
}
|
|
844
|
+
const action = options.append ? 'Appended to' : 'Updated';
|
|
845
|
+
return success(updated, `${action} description for task ${id} (document ${task.descriptionRef}, version ${updated.version})`);
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
// Create new description document
|
|
849
|
+
const docInput = {
|
|
850
|
+
content,
|
|
851
|
+
contentType: ContentType.MARKDOWN,
|
|
852
|
+
createdBy: actor ?? 'operator',
|
|
853
|
+
};
|
|
854
|
+
const newDoc = await createDocument(docInput);
|
|
855
|
+
const created = await api.create(newDoc);
|
|
856
|
+
// Update task with description reference
|
|
857
|
+
await api.update(id, { descriptionRef: created.id }, { actor, expectedUpdatedAt: task.updatedAt });
|
|
858
|
+
const mode = getOutputMode(options);
|
|
859
|
+
if (mode === 'json') {
|
|
860
|
+
return success({ taskId: id, descriptionRef: created.id, document: created });
|
|
861
|
+
}
|
|
862
|
+
if (mode === 'quiet') {
|
|
863
|
+
return success(created.id);
|
|
864
|
+
}
|
|
865
|
+
return success(created, `Created description for task ${id} (document ${created.id})`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
catch (err) {
|
|
869
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
870
|
+
return failure(`Failed to update task description: ${message}`, ExitCode.GENERAL_ERROR);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
export const describeCommand = {
|
|
874
|
+
name: 'describe',
|
|
875
|
+
description: 'Set or show task description',
|
|
876
|
+
usage: 'sf task describe <id> --content <text> | --file <path> | --show',
|
|
877
|
+
help: `Set or show the description for a task.
|
|
878
|
+
|
|
879
|
+
Task descriptions are stored as separate versioned documents. If the task
|
|
880
|
+
already has a description, it will be updated (creating a new version).
|
|
881
|
+
If not, a new document will be created and linked to the task.
|
|
882
|
+
|
|
883
|
+
Arguments:
|
|
884
|
+
id Task identifier (e.g., el-abc123)
|
|
885
|
+
|
|
886
|
+
Options:
|
|
887
|
+
-c, --content <text> Description content (inline)
|
|
888
|
+
-f, --file <path> Read description from file
|
|
889
|
+
-s, --show Show current description instead of setting it
|
|
890
|
+
--append Append to existing description instead of replacing
|
|
891
|
+
|
|
892
|
+
Examples:
|
|
893
|
+
sf task describe el-abc123 --content "Implement the login feature"
|
|
894
|
+
sf task describe el-abc123 --file description.md
|
|
895
|
+
sf task describe el-abc123 --show
|
|
896
|
+
sf task describe el-abc123 --append --content "Additional notes"`,
|
|
897
|
+
options: describeOptions,
|
|
898
|
+
handler: describeHandler,
|
|
899
|
+
};
|
|
900
|
+
// ============================================================================
|
|
901
|
+
// Activate Command
|
|
902
|
+
// ============================================================================
|
|
903
|
+
async function activateHandler(args, options) {
|
|
904
|
+
const [id] = args;
|
|
905
|
+
if (!id) {
|
|
906
|
+
return failure('Usage: sf task activate <id>', ExitCode.INVALID_ARGUMENTS);
|
|
907
|
+
}
|
|
908
|
+
const { api, error } = createAPI(options);
|
|
909
|
+
if (error) {
|
|
910
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
911
|
+
}
|
|
912
|
+
try {
|
|
913
|
+
const task = await api.get(id);
|
|
914
|
+
if (!task) {
|
|
915
|
+
return failure(`Task not found: ${id}`, ExitCode.NOT_FOUND);
|
|
916
|
+
}
|
|
917
|
+
if (task.type !== 'task') {
|
|
918
|
+
return failure(`Element is not a task: ${id}`, ExitCode.VALIDATION);
|
|
919
|
+
}
|
|
920
|
+
if (task.status !== TaskStatus.BACKLOG) {
|
|
921
|
+
return failure(`Task is not in backlog (status: ${task.status})`, ExitCode.VALIDATION);
|
|
922
|
+
}
|
|
923
|
+
const updated = updateTaskStatus(task, {
|
|
924
|
+
status: TaskStatus.OPEN,
|
|
925
|
+
});
|
|
926
|
+
await api.update(id, updated, {
|
|
927
|
+
expectedUpdatedAt: task.updatedAt,
|
|
928
|
+
});
|
|
929
|
+
const mode = getOutputMode(options);
|
|
930
|
+
if (mode === 'json') {
|
|
931
|
+
return success(updated);
|
|
932
|
+
}
|
|
933
|
+
if (mode === 'quiet') {
|
|
934
|
+
return success(updated.id);
|
|
935
|
+
}
|
|
936
|
+
return success(updated, `Activated task ${id} (moved from backlog to open)`);
|
|
937
|
+
}
|
|
938
|
+
catch (err) {
|
|
939
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
940
|
+
return failure(`Failed to activate task: ${message}`, ExitCode.GENERAL_ERROR);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
export const activateCommand = {
|
|
944
|
+
name: 'activate',
|
|
945
|
+
description: 'Move a task from backlog to open',
|
|
946
|
+
usage: 'sf task activate <id>',
|
|
947
|
+
help: `Move a task from backlog status to open status.
|
|
948
|
+
|
|
949
|
+
Arguments:
|
|
950
|
+
id Task identifier
|
|
951
|
+
|
|
952
|
+
Examples:
|
|
953
|
+
sf task activate el-abc123`,
|
|
954
|
+
options: [],
|
|
955
|
+
handler: activateHandler,
|
|
956
|
+
};
|
|
957
|
+
// ============================================================================
|
|
958
|
+
// CRUD Wrapper Commands (delegate to crud.ts handlers with 'task' pre-filled)
|
|
959
|
+
// ============================================================================
|
|
960
|
+
const taskCreateCommand = {
|
|
961
|
+
name: 'create',
|
|
962
|
+
description: 'Create a new task',
|
|
963
|
+
usage: 'sf task create [options]',
|
|
964
|
+
help: `Create a new task.
|
|
965
|
+
|
|
966
|
+
Options:
|
|
967
|
+
-t, --title <text> Task title (required)
|
|
968
|
+
-d, --description <text> Task description (creates a linked document)
|
|
969
|
+
-p, --priority <1-5> Priority (1=critical, 5=minimal, default=3)
|
|
970
|
+
-c, --complexity <1-5> Complexity (1=trivial, 5=very complex, default=3)
|
|
971
|
+
--type <type> Task type: bug, feature, task, chore
|
|
972
|
+
-a, --assignee <id> Assignee entity ID
|
|
973
|
+
--tag <tag> Add a tag (can be repeated)
|
|
974
|
+
--plan <id|name> Plan to attach this task to
|
|
975
|
+
|
|
976
|
+
Examples:
|
|
977
|
+
sf task create --title "Fix login bug" --priority 1 --type bug
|
|
978
|
+
sf task create -t "Add dark mode" --tag ui --tag feature
|
|
979
|
+
sf task create -t "Implement feature X" --plan "My Plan"`,
|
|
980
|
+
options: createOptions,
|
|
981
|
+
handler: ((args, options) => createHandler(['task', ...args], options)),
|
|
982
|
+
};
|
|
983
|
+
const taskListCommand = {
|
|
984
|
+
name: 'list',
|
|
985
|
+
description: 'List tasks',
|
|
986
|
+
usage: 'sf task list [options]',
|
|
987
|
+
help: `List tasks with optional filtering.
|
|
988
|
+
|
|
989
|
+
Options:
|
|
990
|
+
-s, --status <status> Filter by status
|
|
991
|
+
-p, --priority <1-5> Filter by priority
|
|
992
|
+
-a, --assignee <id> Filter by assignee
|
|
993
|
+
--tag <tag> Filter by tag (can be repeated)
|
|
994
|
+
-l, --limit <n> Maximum results (default: 50)
|
|
995
|
+
-o, --offset <n> Skip first n results
|
|
996
|
+
|
|
997
|
+
Examples:
|
|
998
|
+
sf task list
|
|
999
|
+
sf task list --status open
|
|
1000
|
+
sf task list --priority 1 --status in_progress`,
|
|
1001
|
+
options: listOptions,
|
|
1002
|
+
handler: ((args, options) => listHandler(['task', ...args], options)),
|
|
1003
|
+
};
|
|
1004
|
+
const taskShowCommand = {
|
|
1005
|
+
name: 'show',
|
|
1006
|
+
description: 'Show task details',
|
|
1007
|
+
usage: 'sf task show <id> [options]',
|
|
1008
|
+
help: `Show detailed information about a task.
|
|
1009
|
+
|
|
1010
|
+
Arguments:
|
|
1011
|
+
id Task identifier (e.g., el-abc123)
|
|
1012
|
+
|
|
1013
|
+
Options:
|
|
1014
|
+
-e, --events Include recent events/history
|
|
1015
|
+
--events-limit <n> Maximum events to show (default: 10)
|
|
1016
|
+
|
|
1017
|
+
Examples:
|
|
1018
|
+
sf task show el-abc123
|
|
1019
|
+
sf task show el-abc123 --events`,
|
|
1020
|
+
options: showOptions,
|
|
1021
|
+
handler: showHandler,
|
|
1022
|
+
};
|
|
1023
|
+
const taskUpdateCommand = {
|
|
1024
|
+
name: 'update',
|
|
1025
|
+
description: 'Update a task',
|
|
1026
|
+
usage: 'sf task update <id> [options]',
|
|
1027
|
+
help: `Update fields on an existing task.
|
|
1028
|
+
|
|
1029
|
+
Arguments:
|
|
1030
|
+
id Task identifier (e.g., el-abc123)
|
|
1031
|
+
|
|
1032
|
+
Options:
|
|
1033
|
+
-t, --title <text> New title
|
|
1034
|
+
-p, --priority <1-5> New priority
|
|
1035
|
+
-c, --complexity <1-5> New complexity
|
|
1036
|
+
-s, --status <status> New status (open, in_progress, closed, deferred)
|
|
1037
|
+
-a, --assignee <id> New assignee (empty string to unassign)
|
|
1038
|
+
--tag <tag> Replace all tags
|
|
1039
|
+
--add-tag <tag> Add a tag
|
|
1040
|
+
--remove-tag <tag> Remove a tag
|
|
1041
|
+
|
|
1042
|
+
Examples:
|
|
1043
|
+
sf task update el-abc123 --title "New Title"
|
|
1044
|
+
sf task update el-abc123 --priority 1 --status in_progress`,
|
|
1045
|
+
options: updateOptions,
|
|
1046
|
+
handler: updateHandler,
|
|
1047
|
+
};
|
|
1048
|
+
const taskDeleteCommand = {
|
|
1049
|
+
name: 'delete',
|
|
1050
|
+
description: 'Delete a task',
|
|
1051
|
+
usage: 'sf task delete <id> [options]',
|
|
1052
|
+
help: `Soft-delete a task.
|
|
1053
|
+
|
|
1054
|
+
Arguments:
|
|
1055
|
+
id Task identifier (e.g., el-abc123)
|
|
1056
|
+
|
|
1057
|
+
Options:
|
|
1058
|
+
-r, --reason <text> Deletion reason
|
|
1059
|
+
-f, --force Skip confirmation
|
|
1060
|
+
|
|
1061
|
+
Examples:
|
|
1062
|
+
sf task delete el-abc123
|
|
1063
|
+
sf task delete el-abc123 --reason "Duplicate entry"`,
|
|
1064
|
+
options: deleteOptions,
|
|
1065
|
+
handler: deleteHandler,
|
|
1066
|
+
};
|
|
1067
|
+
// ============================================================================
|
|
1068
|
+
// Task Root Command
|
|
1069
|
+
// ============================================================================
|
|
1070
|
+
const allTaskSubcommands = {
|
|
1071
|
+
// CRUD
|
|
1072
|
+
create: taskCreateCommand,
|
|
1073
|
+
list: taskListCommand,
|
|
1074
|
+
show: taskShowCommand,
|
|
1075
|
+
update: taskUpdateCommand,
|
|
1076
|
+
delete: taskDeleteCommand,
|
|
1077
|
+
// Status
|
|
1078
|
+
ready: readyCommand,
|
|
1079
|
+
blocked: blockedCommand,
|
|
1080
|
+
backlog: backlogCommand,
|
|
1081
|
+
close: closeCommand,
|
|
1082
|
+
reopen: reopenCommand,
|
|
1083
|
+
// Assignment
|
|
1084
|
+
assign: assignCommand,
|
|
1085
|
+
// Scheduling
|
|
1086
|
+
defer: deferCommand,
|
|
1087
|
+
undefer: undeferCommand,
|
|
1088
|
+
// Description
|
|
1089
|
+
describe: describeCommand,
|
|
1090
|
+
activate: activateCommand,
|
|
1091
|
+
// Aliases (hidden from --help via dedup in getCommandHelp)
|
|
1092
|
+
new: taskCreateCommand,
|
|
1093
|
+
add: taskCreateCommand,
|
|
1094
|
+
ls: taskListCommand,
|
|
1095
|
+
rm: taskDeleteCommand,
|
|
1096
|
+
get: taskShowCommand,
|
|
1097
|
+
view: taskShowCommand,
|
|
1098
|
+
edit: taskUpdateCommand,
|
|
1099
|
+
};
|
|
1100
|
+
export const taskCommand = {
|
|
1101
|
+
name: 'task',
|
|
1102
|
+
description: 'Task management',
|
|
1103
|
+
usage: 'sf task <subcommand> [options]',
|
|
1104
|
+
help: `Task management - create, list, and manage tasks.
|
|
1105
|
+
|
|
1106
|
+
CRUD:
|
|
1107
|
+
create Create a new task
|
|
1108
|
+
list List tasks
|
|
1109
|
+
show Show task details
|
|
1110
|
+
update Update a task
|
|
1111
|
+
delete Delete a task
|
|
1112
|
+
|
|
1113
|
+
Status:
|
|
1114
|
+
ready List tasks ready for work
|
|
1115
|
+
blocked List blocked tasks with reasons
|
|
1116
|
+
backlog List backlog tasks
|
|
1117
|
+
close Close a task
|
|
1118
|
+
reopen Reopen a closed task
|
|
1119
|
+
activate Move a task from backlog to open
|
|
1120
|
+
|
|
1121
|
+
Assignment:
|
|
1122
|
+
assign Assign a task to an entity
|
|
1123
|
+
|
|
1124
|
+
Scheduling:
|
|
1125
|
+
defer Defer a task
|
|
1126
|
+
undefer Remove deferral from a task
|
|
1127
|
+
|
|
1128
|
+
Description:
|
|
1129
|
+
describe Set or show task description
|
|
1130
|
+
|
|
1131
|
+
Examples:
|
|
1132
|
+
sf task create --title "Fix login bug" --priority 1
|
|
1133
|
+
sf task list --status open
|
|
1134
|
+
sf task ready
|
|
1135
|
+
sf task close el-abc123
|
|
1136
|
+
sf task describe el-abc123 --show`,
|
|
1137
|
+
subcommands: allTaskSubcommands,
|
|
1138
|
+
handler: async (args, _options) => {
|
|
1139
|
+
if (args.length === 0) {
|
|
1140
|
+
return failure('Usage: sf task <subcommand>. Use "sf task --help" for available subcommands.', ExitCode.INVALID_ARGUMENTS);
|
|
1141
|
+
}
|
|
1142
|
+
// Show "did you mean?" for unknown subcommands
|
|
1143
|
+
const subNames = Object.keys(allTaskSubcommands);
|
|
1144
|
+
const suggestions = suggestCommands(args[0], subNames);
|
|
1145
|
+
let msg = `Unknown subcommand: ${args[0]}`;
|
|
1146
|
+
if (suggestions.length > 0) {
|
|
1147
|
+
msg += `\n\nDid you mean?\n${suggestions.map(s => ` ${s}`).join('\n')}`;
|
|
1148
|
+
}
|
|
1149
|
+
msg += '\n\nRun "sf task --help" to see available subcommands.';
|
|
1150
|
+
return failure(msg, ExitCode.INVALID_ARGUMENTS);
|
|
1151
|
+
},
|
|
1152
|
+
};
|
|
1153
|
+
//# sourceMappingURL=task.js.map
|