@agentuity/cli 1.0.29 → 1.0.31
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/agent-detection.d.ts.map +1 -1
- package/dist/agent-detection.js +23 -3
- package/dist/agent-detection.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/cmd/cloud/keyvalue/repl.js +1 -1
- package/dist/cmd/cloud/keyvalue/repl.js.map +1 -1
- package/dist/cmd/cloud/keyvalue/search.js +2 -2
- package/dist/cmd/cloud/keyvalue/search.js.map +1 -1
- package/dist/cmd/cloud/storage/config.d.ts +2 -0
- package/dist/cmd/cloud/storage/config.d.ts.map +1 -0
- package/dist/cmd/cloud/storage/config.js +202 -0
- package/dist/cmd/cloud/storage/config.js.map +1 -0
- package/dist/cmd/cloud/storage/index.d.ts.map +1 -1
- package/dist/cmd/cloud/storage/index.js +2 -0
- package/dist/cmd/cloud/storage/index.js.map +1 -1
- package/dist/cmd/cloud/storage/list.d.ts.map +1 -1
- package/dist/cmd/cloud/storage/list.js +19 -0
- package/dist/cmd/cloud/storage/list.js.map +1 -1
- package/dist/cmd/cloud/task/create.d.ts.map +1 -1
- package/dist/cmd/cloud/task/create.js +15 -6
- package/dist/cmd/cloud/task/create.js.map +1 -1
- package/dist/cmd/cloud/task/delete.d.ts +8 -0
- package/dist/cmd/cloud/task/delete.d.ts.map +1 -0
- package/dist/cmd/cloud/task/delete.js +281 -0
- package/dist/cmd/cloud/task/delete.js.map +1 -0
- package/dist/cmd/cloud/task/get.d.ts.map +1 -1
- package/dist/cmd/cloud/task/get.js +10 -3
- package/dist/cmd/cloud/task/get.js.map +1 -1
- package/dist/cmd/cloud/task/index.d.ts.map +1 -1
- package/dist/cmd/cloud/task/index.js +10 -0
- package/dist/cmd/cloud/task/index.js.map +1 -1
- package/dist/cmd/cloud/task/list.d.ts.map +1 -1
- package/dist/cmd/cloud/task/list.js +2 -0
- package/dist/cmd/cloud/task/list.js.map +1 -1
- package/dist/cmd/project/template-flow.d.ts.map +1 -1
- package/dist/cmd/project/template-flow.js +30 -2
- package/dist/cmd/project/template-flow.js.map +1 -1
- package/dist/domain.d.ts.map +1 -1
- package/dist/domain.js +48 -0
- package/dist/domain.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +3 -1
- package/dist/tui.js.map +1 -1
- package/package.json +6 -6
- package/src/agent-detection.ts +23 -3
- package/src/cli.ts +1 -0
- package/src/cmd/cloud/keyvalue/repl.ts +1 -1
- package/src/cmd/cloud/keyvalue/search.ts +2 -2
- package/src/cmd/cloud/storage/config.ts +238 -0
- package/src/cmd/cloud/storage/index.ts +2 -0
- package/src/cmd/cloud/storage/list.ts +18 -0
- package/src/cmd/cloud/task/create.ts +17 -8
- package/src/cmd/cloud/task/delete.ts +331 -0
- package/src/cmd/cloud/task/get.ts +11 -3
- package/src/cmd/cloud/task/index.ts +10 -0
- package/src/cmd/cloud/task/list.ts +2 -0
- package/src/cmd/project/template-flow.ts +31 -1
- package/src/domain.ts +52 -4
- package/src/tui.ts +2 -1
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
getBucketConfig,
|
|
4
|
+
updateBucketConfig,
|
|
5
|
+
deleteBucketConfig,
|
|
6
|
+
BucketConfigResponseError,
|
|
7
|
+
listOrgResources,
|
|
8
|
+
type BucketConfigUpdate,
|
|
9
|
+
type BucketConfig,
|
|
10
|
+
StorageTierSchema,
|
|
11
|
+
} from '@agentuity/server';
|
|
12
|
+
import { createSubcommand } from '../../../types';
|
|
13
|
+
import * as tui from '../../../tui';
|
|
14
|
+
import { getCatalystAPIClient, getGlobalCatalystAPIClient } from '../../../config';
|
|
15
|
+
import { getCommand } from '../../../command-prefix';
|
|
16
|
+
import { getResourceInfo, setResourceInfo } from '../../../cache';
|
|
17
|
+
|
|
18
|
+
function displayConfig(config: BucketConfig) {
|
|
19
|
+
tui.newline();
|
|
20
|
+
console.log(tui.bold('Bucket: ') + config.bucket_name);
|
|
21
|
+
console.log(
|
|
22
|
+
tui.bold('Storage Tier: ') + (config.storage_tier ?? tui.muted('default'))
|
|
23
|
+
);
|
|
24
|
+
console.log(
|
|
25
|
+
tui.bold('TTL: ') +
|
|
26
|
+
(config.ttl != null ? `${config.ttl}s` : tui.muted('default'))
|
|
27
|
+
);
|
|
28
|
+
console.log(
|
|
29
|
+
tui.bold('Public: ') +
|
|
30
|
+
(config.public != null ? String(config.public) : tui.muted('default'))
|
|
31
|
+
);
|
|
32
|
+
console.log(
|
|
33
|
+
tui.bold('Cache Control: ') + (config.cache_control ?? tui.muted('default'))
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (config.cors) {
|
|
37
|
+
console.log(tui.bold('CORS:'));
|
|
38
|
+
if (config.cors.allowed_origins?.length) {
|
|
39
|
+
console.log(' Origins: ' + config.cors.allowed_origins.join(', '));
|
|
40
|
+
}
|
|
41
|
+
if (config.cors.allowed_methods?.length) {
|
|
42
|
+
console.log(' Methods: ' + config.cors.allowed_methods.join(', '));
|
|
43
|
+
}
|
|
44
|
+
if (config.cors.allowed_headers?.length) {
|
|
45
|
+
console.log(' Headers: ' + config.cors.allowed_headers.join(', '));
|
|
46
|
+
}
|
|
47
|
+
if (config.cors.expose_headers?.length) {
|
|
48
|
+
console.log(' Expose: ' + config.cors.expose_headers.join(', '));
|
|
49
|
+
}
|
|
50
|
+
if (config.cors.max_age_seconds != null) {
|
|
51
|
+
console.log(' Max Age: ' + config.cors.max_age_seconds + 's');
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
console.log(tui.bold('CORS: ') + tui.muted('default'));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (config.additional_headers && Object.keys(config.additional_headers).length > 0) {
|
|
58
|
+
console.log(tui.bold('Headers:'));
|
|
59
|
+
for (const [key, value] of Object.entries(config.additional_headers)) {
|
|
60
|
+
console.log(` ${key}: ${value}`);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
console.log(tui.bold('Headers: ') + tui.muted('default'));
|
|
64
|
+
}
|
|
65
|
+
tui.newline();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const configSubcommand = createSubcommand({
|
|
69
|
+
name: 'config',
|
|
70
|
+
description: 'View or update bucket configuration',
|
|
71
|
+
tags: ['slow', 'requires-auth'],
|
|
72
|
+
requires: { auth: true },
|
|
73
|
+
optional: { org: true },
|
|
74
|
+
idempotent: true,
|
|
75
|
+
examples: [
|
|
76
|
+
{
|
|
77
|
+
command: `${getCommand('cloud storage config')} my-bucket`,
|
|
78
|
+
description: 'View bucket configuration',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
command: `${getCommand('cloud storage config')} my-bucket --ttl 3600 --public`,
|
|
82
|
+
description: 'Update bucket TTL and make it public',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
command: `${getCommand('cloud storage config')} my-bucket --storage-tier ARCHIVE`,
|
|
86
|
+
description: 'Change the storage tier',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
command: `${getCommand('cloud storage config')} my-bucket --reset`,
|
|
90
|
+
description: 'Reset all configuration to system defaults',
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
schema: {
|
|
94
|
+
args: z.object({
|
|
95
|
+
name: z.string().describe('The name of the storage bucket'),
|
|
96
|
+
}),
|
|
97
|
+
options: z.object({
|
|
98
|
+
reset: z.boolean().optional().describe('Reset all configuration to system defaults'),
|
|
99
|
+
storageTier: StorageTierSchema.optional().describe('Storage tier'),
|
|
100
|
+
ttl: z.coerce.number().optional().describe('Object TTL in seconds (0 to clear)'),
|
|
101
|
+
public: z.boolean().optional().describe('Make bucket publicly accessible'),
|
|
102
|
+
cacheControl: z.string().optional().describe('Cache-Control header value'),
|
|
103
|
+
cors: z.string().optional().describe('CORS configuration as JSON string'),
|
|
104
|
+
additionalHeaders: z
|
|
105
|
+
.string()
|
|
106
|
+
.optional()
|
|
107
|
+
.describe('Additional headers as JSON key-value pairs'),
|
|
108
|
+
}),
|
|
109
|
+
response: z.object({
|
|
110
|
+
bucket_name: z.string(),
|
|
111
|
+
storage_tier: z.string().nullable().optional(),
|
|
112
|
+
ttl: z.number().nullable().optional(),
|
|
113
|
+
public: z.boolean().nullable().optional(),
|
|
114
|
+
cache_control: z.string().nullable().optional(),
|
|
115
|
+
cors: z.any().nullable().optional(),
|
|
116
|
+
additional_headers: z.record(z.string(), z.string()).nullable().optional(),
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
async handler(ctx) {
|
|
121
|
+
const { logger, args, opts, options, auth, config } = ctx;
|
|
122
|
+
const { name: bucketName } = args;
|
|
123
|
+
|
|
124
|
+
const profileName = config?.name ?? 'production';
|
|
125
|
+
const catalystClient = await getGlobalCatalystAPIClient(logger, auth, profileName);
|
|
126
|
+
|
|
127
|
+
// Look up bucket to get cloud_region
|
|
128
|
+
const cachedInfo = await getResourceInfo('bucket', profileName, bucketName);
|
|
129
|
+
const orgId = ctx.orgId ?? cachedInfo?.orgId;
|
|
130
|
+
|
|
131
|
+
const resources = await tui.spinner({
|
|
132
|
+
message: 'Looking up bucket...',
|
|
133
|
+
clearOnSuccess: true,
|
|
134
|
+
callback: () => listOrgResources(catalystClient, { type: 's3', orgId }),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const bucket = resources.s3.find((s3) => s3.bucket_name === bucketName);
|
|
138
|
+
if (!bucket) {
|
|
139
|
+
throw new BucketConfigResponseError({ message: `Bucket "${bucketName}" not found` });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Cache the bucket info for future lookups
|
|
143
|
+
if (bucket.cloud_region && bucket.org_id) {
|
|
144
|
+
await setResourceInfo(
|
|
145
|
+
'bucket',
|
|
146
|
+
profileName,
|
|
147
|
+
bucket.bucket_name,
|
|
148
|
+
bucket.cloud_region,
|
|
149
|
+
bucket.org_id
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!bucket.cloud_region) {
|
|
154
|
+
throw new BucketConfigResponseError({
|
|
155
|
+
message: `Bucket "${bucketName}" is missing region information`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Create regional client for bucket config operations (orgId required for CLI auth)
|
|
160
|
+
const regionalClient = getCatalystAPIClient(logger, auth, bucket.cloud_region, bucket.org_id);
|
|
161
|
+
|
|
162
|
+
// Handle --reset flag (DELETE)
|
|
163
|
+
if (opts.reset) {
|
|
164
|
+
await tui.spinner({
|
|
165
|
+
message: 'Resetting bucket configuration...',
|
|
166
|
+
clearOnSuccess: true,
|
|
167
|
+
callback: () => deleteBucketConfig(regionalClient, bucketName),
|
|
168
|
+
});
|
|
169
|
+
if (!options.json) {
|
|
170
|
+
tui.success(`Configuration reset to defaults for bucket "${bucketName}"`);
|
|
171
|
+
}
|
|
172
|
+
return { bucket_name: bucketName };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check if any update flags are present
|
|
176
|
+
const hasUpdateFlags =
|
|
177
|
+
opts.storageTier !== undefined ||
|
|
178
|
+
opts.ttl !== undefined ||
|
|
179
|
+
opts.public !== undefined ||
|
|
180
|
+
opts.cacheControl !== undefined ||
|
|
181
|
+
opts.cors !== undefined ||
|
|
182
|
+
opts.additionalHeaders !== undefined;
|
|
183
|
+
|
|
184
|
+
if (hasUpdateFlags) {
|
|
185
|
+
// Build update payload
|
|
186
|
+
const update: BucketConfigUpdate = {};
|
|
187
|
+
|
|
188
|
+
if (opts.storageTier !== undefined) update.storage_tier = opts.storageTier;
|
|
189
|
+
if (opts.ttl !== undefined) update.ttl = opts.ttl === 0 ? null : opts.ttl;
|
|
190
|
+
if (opts.public !== undefined) update.public = opts.public;
|
|
191
|
+
if (opts.cacheControl !== undefined) update.cache_control = opts.cacheControl;
|
|
192
|
+
|
|
193
|
+
// Parse JSON flags
|
|
194
|
+
if (opts.cors !== undefined) {
|
|
195
|
+
try {
|
|
196
|
+
update.cors = JSON.parse(opts.cors);
|
|
197
|
+
} catch {
|
|
198
|
+
throw new BucketConfigResponseError({
|
|
199
|
+
message: 'Invalid JSON for --cors flag',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (opts.additionalHeaders !== undefined) {
|
|
204
|
+
try {
|
|
205
|
+
update.additional_headers = JSON.parse(opts.additionalHeaders);
|
|
206
|
+
} catch {
|
|
207
|
+
throw new BucketConfigResponseError({
|
|
208
|
+
message: 'Invalid JSON for --additionalHeaders flag',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const result = await tui.spinner({
|
|
214
|
+
message: 'Updating bucket configuration...',
|
|
215
|
+
clearOnSuccess: true,
|
|
216
|
+
callback: () => updateBucketConfig(regionalClient, bucketName, update),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (!options.json) {
|
|
220
|
+
displayConfig(result);
|
|
221
|
+
tui.success(`Configuration updated for bucket "${bucketName}"`);
|
|
222
|
+
}
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// No update flags — GET and display
|
|
227
|
+
const getResult = await tui.spinner({
|
|
228
|
+
message: 'Fetching bucket configuration...',
|
|
229
|
+
clearOnSuccess: true,
|
|
230
|
+
callback: () => getBucketConfig(regionalClient, bucketName),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (!options.json) {
|
|
234
|
+
displayConfig(getResult);
|
|
235
|
+
}
|
|
236
|
+
return getResult;
|
|
237
|
+
},
|
|
238
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createCommand } from '../../../types';
|
|
2
|
+
import { configSubcommand } from './config';
|
|
2
3
|
import { createSubcommand } from './create';
|
|
3
4
|
import { listSubcommand } from './list';
|
|
4
5
|
import { deleteSubcommand } from './delete';
|
|
@@ -20,6 +21,7 @@ export const storageCommand = createCommand({
|
|
|
20
21
|
},
|
|
21
22
|
],
|
|
22
23
|
subcommands: [
|
|
24
|
+
configSubcommand,
|
|
23
25
|
createSubcommand,
|
|
24
26
|
listSubcommand,
|
|
25
27
|
getSubcommand,
|
|
@@ -23,6 +23,9 @@ const StorageListResponseSchema = z.object({
|
|
|
23
23
|
bucket_type: z.string().optional().describe('Bucket type (user or snapshots)'),
|
|
24
24
|
internal: z.boolean().optional().describe('Whether this is a system-managed bucket'),
|
|
25
25
|
description: z.string().optional().describe('Optional description of the bucket'),
|
|
26
|
+
object_count: z.number().int().optional().describe('Number of objects in this bucket'),
|
|
27
|
+
total_size: z.number().int().optional().describe('Total size of objects in bytes'),
|
|
28
|
+
last_event_at: z.string().optional().describe('Last activity timestamp'),
|
|
26
29
|
})
|
|
27
30
|
)
|
|
28
31
|
.optional()
|
|
@@ -256,6 +259,18 @@ export const listSubcommand = createSubcommand({
|
|
|
256
259
|
}
|
|
257
260
|
if (s3.region) console.log(` Region: ${tui.muted(s3.region)}`);
|
|
258
261
|
if (s3.endpoint) console.log(` Endpoint: ${tui.muted(s3.endpoint)}`);
|
|
262
|
+
if (s3.object_count != null) {
|
|
263
|
+
const sizeStr = s3.total_size != null ? tui.formatBytes(s3.total_size) : 'unknown';
|
|
264
|
+
console.log(` Objects: ${tui.muted(`${s3.object_count.toLocaleString()} (${sizeStr})`)}`);
|
|
265
|
+
}
|
|
266
|
+
if (s3.last_event_at) {
|
|
267
|
+
const date = new Date(s3.last_event_at);
|
|
268
|
+
if (Number.isNaN(date.getTime())) {
|
|
269
|
+
console.log(` Activity: ${tui.muted('unknown')}`);
|
|
270
|
+
} else {
|
|
271
|
+
console.log(` Activity: ${tui.muted(date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' }))}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
259
274
|
tui.newline();
|
|
260
275
|
}
|
|
261
276
|
}
|
|
@@ -274,6 +289,9 @@ export const listSubcommand = createSubcommand({
|
|
|
274
289
|
bucket_type: s3.bucket_type,
|
|
275
290
|
internal: s3.internal,
|
|
276
291
|
description: s3.description ?? undefined,
|
|
292
|
+
object_count: s3.object_count ?? undefined,
|
|
293
|
+
total_size: s3.total_size ?? undefined,
|
|
294
|
+
last_event_at: s3.last_event_at ?? undefined,
|
|
277
295
|
})),
|
|
278
296
|
};
|
|
279
297
|
},
|
|
@@ -5,7 +5,7 @@ import * as tui from '../../../tui';
|
|
|
5
5
|
import { createStorageAdapter, parseMetadataFlag, cacheTaskId } from './util';
|
|
6
6
|
import { getCommand } from '../../../command-prefix';
|
|
7
7
|
import { whoami } from '@agentuity/server';
|
|
8
|
-
import type { TaskPriority, TaskStatus, TaskType } from '@agentuity/core';
|
|
8
|
+
import type { TaskPriority, TaskStatus, TaskType, UserType } from '@agentuity/core';
|
|
9
9
|
import { getCachedUserInfo, setCachedUserInfo } from '../../../cache';
|
|
10
10
|
import { defaultProfileName } from '../../../config';
|
|
11
11
|
|
|
@@ -70,6 +70,10 @@ export const createSubcommand = createCommand({
|
|
|
70
70
|
.min(1)
|
|
71
71
|
.optional()
|
|
72
72
|
.describe('the display name of the creator (used with --created-id)'),
|
|
73
|
+
createdType: z
|
|
74
|
+
.enum(['human', 'agent'])
|
|
75
|
+
.optional()
|
|
76
|
+
.describe('the type of the creator - human user or AI agent (default: human)'),
|
|
73
77
|
projectId: z.string().optional().describe('project ID to associate with the task'),
|
|
74
78
|
projectName: z
|
|
75
79
|
.string()
|
|
@@ -101,18 +105,23 @@ export const createSubcommand = createCommand({
|
|
|
101
105
|
|
|
102
106
|
// Resolve creator info
|
|
103
107
|
const createdId = opts.createdId ?? ctx.auth.userId;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
creator
|
|
108
|
-
|
|
108
|
+
const createdType = (opts.createdType as UserType) ?? 'human';
|
|
109
|
+
let creator: { id: string; name: string; type?: UserType } | undefined;
|
|
110
|
+
if (opts.createdId) {
|
|
111
|
+
// Explicit creator — use createdId as name fallback (like project pattern)
|
|
112
|
+
creator = {
|
|
113
|
+
id: opts.createdId,
|
|
114
|
+
name: opts.createdName ?? opts.createdId,
|
|
115
|
+
type: createdType,
|
|
116
|
+
};
|
|
117
|
+
} else {
|
|
109
118
|
// Using auth userId — check cache first, then fall back to whoami API call
|
|
110
119
|
const profileName = ctx.config?.name ?? defaultProfileName;
|
|
111
120
|
const cached = getCachedUserInfo(profileName);
|
|
112
121
|
if (cached) {
|
|
113
122
|
const name = [cached.firstName, cached.lastName].filter(Boolean).join(' ');
|
|
114
123
|
if (name) {
|
|
115
|
-
creator = { id: createdId, name };
|
|
124
|
+
creator = { id: createdId, name, type: createdType };
|
|
116
125
|
}
|
|
117
126
|
} else {
|
|
118
127
|
// Fetch from API and cache
|
|
@@ -120,7 +129,7 @@ export const createSubcommand = createCommand({
|
|
|
120
129
|
const user = await whoami(ctx.apiClient);
|
|
121
130
|
const name = [user.firstName, user.lastName].filter(Boolean).join(' ');
|
|
122
131
|
if (name) {
|
|
123
|
-
creator = { id: createdId, name };
|
|
132
|
+
creator = { id: createdId, name, type: createdType };
|
|
124
133
|
}
|
|
125
134
|
setCachedUserInfo(profileName, createdId, user.firstName, user.lastName);
|
|
126
135
|
} catch {
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createCommand } from '../../../types';
|
|
3
|
+
import * as tui from '../../../tui';
|
|
4
|
+
import { createStorageAdapter } from './util';
|
|
5
|
+
import { getCommand } from '../../../command-prefix';
|
|
6
|
+
import { isDryRunMode, outputDryRun } from '../../../explain';
|
|
7
|
+
import type { TaskPriority, TaskStatus, TaskType, BatchDeletedTask } from '@agentuity/core';
|
|
8
|
+
|
|
9
|
+
const DURATION_UNITS: Record<string, number> = {
|
|
10
|
+
s: 1000,
|
|
11
|
+
m: 60 * 1000,
|
|
12
|
+
h: 60 * 60 * 1000,
|
|
13
|
+
d: 24 * 60 * 60 * 1000,
|
|
14
|
+
w: 7 * 24 * 60 * 60 * 1000,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a human-friendly duration string (e.g. "30s", "7d", "24h", "30m", "2w")
|
|
19
|
+
* into milliseconds. Exported for testing.
|
|
20
|
+
*/
|
|
21
|
+
export function parseDuration(duration: string): number {
|
|
22
|
+
const match = duration.match(/^(\d+)([smhdw])$/);
|
|
23
|
+
if (!match) {
|
|
24
|
+
tui.fatal(
|
|
25
|
+
`Invalid duration format: "${duration}". Use a number followed by s (seconds), m (minutes), h (hours), d (days), or w (weeks). Examples: 30s, 30m, 24h, 7d, 2w`
|
|
26
|
+
);
|
|
27
|
+
// tui.fatal exits, but TypeScript doesn't know that
|
|
28
|
+
throw new Error('unreachable');
|
|
29
|
+
}
|
|
30
|
+
const value = parseInt(match[1]!, 10);
|
|
31
|
+
const unit = match[2]!;
|
|
32
|
+
const ms = DURATION_UNITS[unit];
|
|
33
|
+
if (!ms) {
|
|
34
|
+
tui.fatal(`Unknown duration unit: "${unit}"`);
|
|
35
|
+
throw new Error('unreachable');
|
|
36
|
+
}
|
|
37
|
+
return value * ms;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function truncate(s: string, max: number): string {
|
|
41
|
+
if (s.length <= max) return s;
|
|
42
|
+
return `${s.slice(0, max - 1)}…`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const TaskDeleteResponseSchema = z.object({
|
|
46
|
+
success: z.boolean().describe('Whether the operation succeeded'),
|
|
47
|
+
deleted: z
|
|
48
|
+
.array(
|
|
49
|
+
z.object({
|
|
50
|
+
id: z.string().describe('Deleted task ID'),
|
|
51
|
+
title: z.string().describe('Deleted task title'),
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
.describe('List of deleted tasks'),
|
|
55
|
+
count: z.number().describe('Number of tasks deleted'),
|
|
56
|
+
durationMs: z.number().describe('Operation duration in milliseconds'),
|
|
57
|
+
dryRun: z.boolean().optional().describe('Whether this was a dry run'),
|
|
58
|
+
message: z.string().optional().describe('Status message'),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const deleteSubcommand = createCommand({
|
|
62
|
+
name: 'delete',
|
|
63
|
+
aliases: ['del', 'rm'],
|
|
64
|
+
description: 'Soft-delete a task by ID or batch-delete tasks by filter',
|
|
65
|
+
tags: ['destructive', 'deletes-resource', 'slow', 'requires-auth'],
|
|
66
|
+
requires: { auth: true },
|
|
67
|
+
examples: [
|
|
68
|
+
{
|
|
69
|
+
command: getCommand('cloud task delete task_abc123'),
|
|
70
|
+
description: 'Delete a single task by ID',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
command: getCommand('cloud task delete --status closed --older-than 7d'),
|
|
74
|
+
description: 'Delete closed tasks older than 7 days',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
command: getCommand('cloud task delete --status done --limit 10 --dry-run'),
|
|
78
|
+
description: 'Preview which done tasks would be deleted (dry run)',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
command: getCommand('cloud task delete --status cancelled --confirm'),
|
|
82
|
+
description: 'Delete all cancelled tasks without confirmation prompt',
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
schema: {
|
|
86
|
+
args: z.object({
|
|
87
|
+
id: z.string().optional().describe('Task ID to delete (for single delete)'),
|
|
88
|
+
}),
|
|
89
|
+
options: z.object({
|
|
90
|
+
status: z
|
|
91
|
+
.enum(['open', 'in_progress', 'done', 'closed', 'cancelled'])
|
|
92
|
+
.optional()
|
|
93
|
+
.describe('filter batch delete by status'),
|
|
94
|
+
type: z
|
|
95
|
+
.enum(['epic', 'feature', 'enhancement', 'bug', 'task'])
|
|
96
|
+
.optional()
|
|
97
|
+
.describe('filter batch delete by type'),
|
|
98
|
+
priority: z
|
|
99
|
+
.enum(['high', 'medium', 'low', 'none'])
|
|
100
|
+
.optional()
|
|
101
|
+
.describe('filter batch delete by priority'),
|
|
102
|
+
olderThan: z
|
|
103
|
+
.string()
|
|
104
|
+
.optional()
|
|
105
|
+
.describe('filter batch delete by age (e.g. 30s, 7d, 24h, 2w)'),
|
|
106
|
+
parentId: z.string().optional().describe('filter batch delete by parent task ID'),
|
|
107
|
+
createdId: z.string().optional().describe('filter batch delete by creator ID'),
|
|
108
|
+
limit: z.coerce
|
|
109
|
+
.number()
|
|
110
|
+
.int()
|
|
111
|
+
.min(1)
|
|
112
|
+
.max(200)
|
|
113
|
+
.default(50)
|
|
114
|
+
.describe('max tasks to delete in batch mode (default: 50, max: 200)'),
|
|
115
|
+
confirm: z.boolean().optional().default(false).describe('skip confirmation prompt'),
|
|
116
|
+
}),
|
|
117
|
+
response: TaskDeleteResponseSchema,
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
async handler(ctx) {
|
|
121
|
+
const { args, opts, options } = ctx;
|
|
122
|
+
const started = Date.now();
|
|
123
|
+
const storage = await createStorageAdapter(ctx);
|
|
124
|
+
|
|
125
|
+
// Determine mode: single delete or batch delete
|
|
126
|
+
const isSingleDelete = !!args.id;
|
|
127
|
+
const hasFilters =
|
|
128
|
+
opts.status || opts.type || opts.priority || opts.olderThan || opts.parentId || opts.createdId;
|
|
129
|
+
|
|
130
|
+
if (!isSingleDelete && !hasFilters) {
|
|
131
|
+
tui.fatal(
|
|
132
|
+
'Provide a task ID for single delete, or use --status, --type, --priority, --older-than, --parent-id, or --created-id for batch delete.'
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (isSingleDelete && hasFilters) {
|
|
137
|
+
tui.fatal(
|
|
138
|
+
'Cannot combine task ID with filter options. Use either single delete (by ID) or batch delete (by filters).'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Single delete mode ──────────────────────────────────────────────
|
|
143
|
+
if (isSingleDelete) {
|
|
144
|
+
if (isDryRunMode(options)) {
|
|
145
|
+
outputDryRun(`Would soft-delete task: ${args.id}`, options);
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
deleted: [{ id: args.id!, title: '(dry run)' }],
|
|
149
|
+
count: 1,
|
|
150
|
+
durationMs: Date.now() - started,
|
|
151
|
+
dryRun: true,
|
|
152
|
+
message: 'Dry run — no tasks were deleted',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!opts.confirm) {
|
|
157
|
+
const confirmed = await tui.confirm(`Delete task "${args.id}"?`, false);
|
|
158
|
+
if (!confirmed) {
|
|
159
|
+
if (!options.json) tui.info('Cancelled');
|
|
160
|
+
return {
|
|
161
|
+
success: false,
|
|
162
|
+
deleted: [],
|
|
163
|
+
count: 0,
|
|
164
|
+
durationMs: Date.now() - started,
|
|
165
|
+
message: 'Cancelled',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const task = await storage.softDelete(args.id!);
|
|
171
|
+
const durationMs = Date.now() - started;
|
|
172
|
+
|
|
173
|
+
if (!options.json) {
|
|
174
|
+
tui.success(`Deleted task ${tui.bold(task.id)} (${task.title}) in ${durationMs}ms`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
success: true,
|
|
179
|
+
deleted: [{ id: task.id, title: task.title }],
|
|
180
|
+
count: 1,
|
|
181
|
+
durationMs,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Batch delete mode ───────────────────────────────────────────────
|
|
186
|
+
// Validate older-than format early (before calling the API)
|
|
187
|
+
if (opts.olderThan) {
|
|
188
|
+
parseDuration(opts.olderThan); // will fatal on invalid format
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const batchParams = {
|
|
192
|
+
status: opts.status as TaskStatus | undefined,
|
|
193
|
+
type: opts.type as TaskType | undefined,
|
|
194
|
+
priority: opts.priority as TaskPriority | undefined,
|
|
195
|
+
parent_id: opts.parentId,
|
|
196
|
+
created_id: opts.createdId,
|
|
197
|
+
older_than: opts.olderThan,
|
|
198
|
+
limit: opts.limit,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// For dry-run and preview, first list what would be matched
|
|
202
|
+
// (we call batchDelete only when actually executing)
|
|
203
|
+
if (isDryRunMode(options) || !opts.confirm) {
|
|
204
|
+
// Use list() to preview matching tasks
|
|
205
|
+
const preview = await storage.list({
|
|
206
|
+
status: batchParams.status,
|
|
207
|
+
type: batchParams.type,
|
|
208
|
+
priority: batchParams.priority,
|
|
209
|
+
parent_id: batchParams.parent_id,
|
|
210
|
+
limit: batchParams.limit,
|
|
211
|
+
sort: 'created_at',
|
|
212
|
+
order: 'asc',
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Client-side filters for preview (server will apply these on actual delete)
|
|
216
|
+
let candidates = preview.tasks;
|
|
217
|
+
if (batchParams.created_id) {
|
|
218
|
+
candidates = candidates.filter(
|
|
219
|
+
(t: { created_id: string }) => t.created_id === batchParams.created_id
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (opts.olderThan) {
|
|
223
|
+
const durationMs = parseDuration(opts.olderThan);
|
|
224
|
+
const cutoff = new Date(Date.now() - durationMs);
|
|
225
|
+
candidates = candidates.filter(
|
|
226
|
+
(t: { created_at: string }) => new Date(t.created_at) < cutoff
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (candidates.length === 0) {
|
|
231
|
+
if (!options.json) tui.info('No tasks match the given filters');
|
|
232
|
+
return {
|
|
233
|
+
success: true,
|
|
234
|
+
deleted: [],
|
|
235
|
+
count: 0,
|
|
236
|
+
durationMs: Date.now() - started,
|
|
237
|
+
message: 'No matching tasks found',
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Show preview table
|
|
242
|
+
if (!options.json) {
|
|
243
|
+
tui.warning(
|
|
244
|
+
`Found ${candidates.length} ${tui.plural(candidates.length, 'task', 'tasks')} to delete:`
|
|
245
|
+
);
|
|
246
|
+
tui.newline();
|
|
247
|
+
|
|
248
|
+
const tableData = candidates.map(
|
|
249
|
+
(task: { id: string; title: string; status: string; type: string; created_at: string }) => ({
|
|
250
|
+
ID: tui.muted(truncate(task.id, 28)),
|
|
251
|
+
Title: truncate(task.title, 40),
|
|
252
|
+
Status: task.status,
|
|
253
|
+
Type: task.type,
|
|
254
|
+
Created: new Date(task.created_at).toLocaleDateString(),
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
tui.table(tableData, [
|
|
259
|
+
{ name: 'ID', alignment: 'left' },
|
|
260
|
+
{ name: 'Title', alignment: 'left' },
|
|
261
|
+
{ name: 'Status', alignment: 'left' },
|
|
262
|
+
{ name: 'Type', alignment: 'left' },
|
|
263
|
+
{ name: 'Created', alignment: 'left' },
|
|
264
|
+
]);
|
|
265
|
+
tui.newline();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Dry-run: return preview without executing
|
|
269
|
+
if (isDryRunMode(options)) {
|
|
270
|
+
outputDryRun(
|
|
271
|
+
`Would soft-delete ${candidates.length} ${tui.plural(candidates.length, 'task', 'tasks')}`,
|
|
272
|
+
options
|
|
273
|
+
);
|
|
274
|
+
return {
|
|
275
|
+
success: true,
|
|
276
|
+
deleted: candidates.map(
|
|
277
|
+
(t: { id: string; title: string }): BatchDeletedTask => ({
|
|
278
|
+
id: t.id,
|
|
279
|
+
title: t.title,
|
|
280
|
+
})
|
|
281
|
+
),
|
|
282
|
+
count: candidates.length,
|
|
283
|
+
durationMs: Date.now() - started,
|
|
284
|
+
dryRun: true,
|
|
285
|
+
message: 'Dry run — no tasks were deleted',
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Confirmation prompt
|
|
290
|
+
if (!opts.confirm) {
|
|
291
|
+
const confirmed = await tui.confirm(
|
|
292
|
+
`Delete ${candidates.length} ${tui.plural(candidates.length, 'task', 'tasks')}?`,
|
|
293
|
+
false
|
|
294
|
+
);
|
|
295
|
+
if (!confirmed) {
|
|
296
|
+
if (!options.json) tui.info('Cancelled');
|
|
297
|
+
return {
|
|
298
|
+
success: false,
|
|
299
|
+
deleted: [],
|
|
300
|
+
count: 0,
|
|
301
|
+
durationMs: Date.now() - started,
|
|
302
|
+
message: 'Cancelled',
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Execute batch delete via server-side API
|
|
309
|
+
const result = await storage.batchDelete(batchParams);
|
|
310
|
+
const durationMs = Date.now() - started;
|
|
311
|
+
|
|
312
|
+
if (!options.json) {
|
|
313
|
+
if (result.count > 0) {
|
|
314
|
+
tui.success(
|
|
315
|
+
`Deleted ${result.count} ${tui.plural(result.count, 'task', 'tasks')} in ${durationMs}ms`
|
|
316
|
+
);
|
|
317
|
+
} else {
|
|
318
|
+
tui.info('No tasks matched the given filters');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
success: true,
|
|
324
|
+
deleted: result.deleted,
|
|
325
|
+
count: result.count,
|
|
326
|
+
durationMs,
|
|
327
|
+
};
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
export default deleteSubcommand;
|