@agentuity/cli 2.0.5 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/cmd/build/patch/otel-llm.js +2 -2
- package/dist/cmd/build/patch/otel-llm.js.map +1 -1
- package/dist/cmd/build/vite/bun-dev-server.d.ts.map +1 -1
- package/dist/cmd/build/vite/bun-dev-server.js +1 -0
- package/dist/cmd/build/vite/bun-dev-server.js.map +1 -1
- package/dist/cmd/build/vite/index.d.ts +0 -28
- package/dist/cmd/build/vite/index.d.ts.map +1 -1
- package/dist/cmd/build/vite/index.js +1 -104
- package/dist/cmd/build/vite/index.js.map +1 -1
- package/dist/cmd/build/vite/metadata-generator.d.ts.map +1 -1
- package/dist/cmd/build/vite/metadata-generator.js +8 -2
- package/dist/cmd/build/vite/metadata-generator.js.map +1 -1
- package/dist/cmd/build/vite/vite-asset-server-config.d.ts +2 -0
- package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
- package/dist/cmd/build/vite/vite-asset-server-config.js +5 -1
- package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
- package/dist/cmd/build/vite/vite-asset-server.d.ts +2 -0
- package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -1
- package/dist/cmd/build/vite/vite-asset-server.js +2 -1
- package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
- package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
- package/dist/cmd/build/vite/vite-builder.js +143 -2
- package/dist/cmd/build/vite/vite-builder.js.map +1 -1
- package/dist/cmd/cloud/task/close.d.ts +3 -0
- package/dist/cmd/cloud/task/close.d.ts.map +1 -0
- package/dist/cmd/cloud/task/close.js +286 -0
- package/dist/cmd/cloud/task/close.js.map +1 -0
- package/dist/cmd/cloud/task/delete.d.ts +1 -5
- package/dist/cmd/cloud/task/delete.d.ts.map +1 -1
- package/dist/cmd/cloud/task/delete.js +15 -38
- package/dist/cmd/cloud/task/delete.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 +97 -3
- package/dist/cmd/cloud/task/list.js.map +1 -1
- package/dist/cmd/cloud/task/util.d.ts +10 -0
- package/dist/cmd/cloud/task/util.d.ts.map +1 -1
- package/dist/cmd/cloud/task/util.js +47 -3
- package/dist/cmd/cloud/task/util.js.map +1 -1
- package/dist/cmd/coder/config/index.d.ts +2 -0
- package/dist/cmd/coder/config/index.d.ts.map +1 -0
- package/dist/cmd/coder/config/index.js +20 -0
- package/dist/cmd/coder/config/index.js.map +1 -0
- package/dist/cmd/coder/config/set.d.ts +2 -0
- package/dist/cmd/coder/config/set.d.ts.map +1 -0
- package/dist/cmd/coder/config/set.js +100 -0
- package/dist/cmd/coder/config/set.js.map +1 -0
- package/dist/cmd/coder/hub-url.d.ts +21 -10
- package/dist/cmd/coder/hub-url.d.ts.map +1 -1
- package/dist/cmd/coder/hub-url.js +97 -55
- package/dist/cmd/coder/hub-url.js.map +1 -1
- package/dist/cmd/coder/index.d.ts.map +1 -1
- package/dist/cmd/coder/index.js +6 -1
- package/dist/cmd/coder/index.js.map +1 -1
- package/dist/cmd/coder/inspect.d.ts.map +1 -1
- package/dist/cmd/coder/inspect.js +15 -7
- package/dist/cmd/coder/inspect.js.map +1 -1
- package/dist/cmd/coder/list.d.ts.map +1 -1
- package/dist/cmd/coder/list.js +14 -7
- package/dist/cmd/coder/list.js.map +1 -1
- package/dist/cmd/coder/start.d.ts.map +1 -1
- package/dist/cmd/coder/start.js +38 -23
- package/dist/cmd/coder/start.js.map +1 -1
- package/dist/cmd/coder/tui-init.d.ts +4 -1
- package/dist/cmd/coder/tui-init.d.ts.map +1 -1
- package/dist/cmd/coder/tui-init.js +3 -2
- package/dist/cmd/coder/tui-init.js.map +1 -1
- package/dist/cmd/dev/index.d.ts.map +1 -1
- package/dist/cmd/dev/index.js +1 -0
- package/dist/cmd/dev/index.js.map +1 -1
- package/dist/cmd/dev/sync.js +5 -5
- package/dist/cmd/dev/sync.js.map +1 -1
- package/dist/coder-config.d.ts +14 -0
- package/dist/coder-config.d.ts.map +1 -0
- package/dist/coder-config.js +119 -0
- package/dist/coder-config.js.map +1 -0
- package/dist/coder-hub-url.d.ts +3 -0
- package/dist/coder-hub-url.d.ts.map +1 -0
- package/dist/coder-hub-url.js +32 -0
- package/dist/coder-hub-url.js.map +1 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/internal-logger.d.ts +4 -0
- package/dist/internal-logger.d.ts.map +1 -1
- package/dist/internal-logger.js +64 -2
- package/dist/internal-logger.js.map +1 -1
- package/dist/keychain.d.ts +3 -0
- package/dist/keychain.d.ts.map +1 -1
- package/dist/keychain.js +47 -28
- package/dist/keychain.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/package.json +6 -6
- package/src/cmd/build/patch/otel-llm.ts +2 -2
- package/src/cmd/build/vite/bun-dev-server.ts +1 -0
- package/src/cmd/build/vite/index.ts +1 -148
- package/src/cmd/build/vite/metadata-generator.ts +8 -2
- package/src/cmd/build/vite/vite-asset-server-config.ts +16 -1
- package/src/cmd/build/vite/vite-asset-server.ts +4 -0
- package/src/cmd/build/vite/vite-builder.ts +171 -9
- package/src/cmd/cloud/task/close.ts +319 -0
- package/src/cmd/cloud/task/delete.ts +15 -43
- package/src/cmd/cloud/task/index.ts +10 -0
- package/src/cmd/cloud/task/list.ts +111 -4
- package/src/cmd/cloud/task/util.ts +59 -5
- package/src/cmd/coder/config/index.ts +20 -0
- package/src/cmd/coder/config/set.ts +112 -0
- package/src/cmd/coder/hub-url.ts +147 -53
- package/src/cmd/coder/index.ts +6 -1
- package/src/cmd/coder/inspect.ts +33 -10
- package/src/cmd/coder/list.ts +33 -10
- package/src/cmd/coder/start.ts +62 -26
- package/src/cmd/coder/tui-init.ts +7 -2
- package/src/cmd/dev/index.ts +1 -0
- package/src/cmd/dev/sync.ts +5 -5
- package/src/coder-config.ts +141 -0
- package/src/coder-hub-url.ts +32 -0
- package/src/config.ts +13 -0
- package/src/internal-logger.ts +83 -2
- package/src/keychain.ts +68 -39
- package/src/types.ts +10 -0
|
@@ -5,11 +5,132 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { join } from 'node:path';
|
|
8
|
-
import { existsSync, rmSync } from 'node:fs';
|
|
8
|
+
import { existsSync, rmSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
9
11
|
import type { InlineConfig } from 'vite';
|
|
10
12
|
import type { Logger, DeployOptions } from '../../../types';
|
|
11
13
|
import type { BuildReportCollector } from '../../../build-report';
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Read the pre-built beacon script from @agentuity/frontend package.
|
|
17
|
+
* Tries multiple resolution strategies for workspace/installed/symlink scenarios.
|
|
18
|
+
*/
|
|
19
|
+
async function readBeaconScript(projectRoot: string): Promise<string> {
|
|
20
|
+
let frontendPath: string | null = null;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
frontendPath = await Bun.resolve('@agentuity/frontend', projectRoot);
|
|
24
|
+
} catch {
|
|
25
|
+
// Not found from project root
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!frontendPath) {
|
|
29
|
+
try {
|
|
30
|
+
const thisDir = new URL('.', import.meta.url).pathname;
|
|
31
|
+
frontendPath = await Bun.resolve('@agentuity/frontend', thisDir);
|
|
32
|
+
} catch {
|
|
33
|
+
// Not found from CLI directory
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!frontendPath) {
|
|
38
|
+
try {
|
|
39
|
+
const projectRequire = createRequire(join(projectRoot, 'package.json'));
|
|
40
|
+
frontendPath = projectRequire.resolve('@agentuity/frontend');
|
|
41
|
+
} catch {
|
|
42
|
+
// Not found via createRequire
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!frontendPath) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'Could not resolve @agentuity/frontend. Ensure the package is installed and built.'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const packageDir = join(frontendPath, '..');
|
|
53
|
+
const beaconPath = join(packageDir, 'beacon.js');
|
|
54
|
+
|
|
55
|
+
const beaconFile = Bun.file(beaconPath);
|
|
56
|
+
if (!(await beaconFile.exists())) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Beacon script not found at ${beaconPath}. Run "bun run build" in @agentuity/frontend first.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return beaconFile.text();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Post-build step: inject the analytics beacon into the built index.html.
|
|
67
|
+
*
|
|
68
|
+
* 1. Reads the beacon script from @agentuity/frontend
|
|
69
|
+
* 2. Writes it as a content-hashed asset file
|
|
70
|
+
* 3. Injects a <script data-agentuity-beacon> tag into the HTML
|
|
71
|
+
*
|
|
72
|
+
* This runs after `vite build` completes so it works regardless of the
|
|
73
|
+
* user's vite.config.ts — no Vite plugin required.
|
|
74
|
+
*/
|
|
75
|
+
async function injectBeacon(rootDir: string, cdnBaseUrl: string, logger: Logger): Promise<void> {
|
|
76
|
+
const clientDir = join(rootDir, '.agentuity/client');
|
|
77
|
+
const indexHtmlPath = join(clientDir, 'index.html');
|
|
78
|
+
|
|
79
|
+
if (!existsSync(indexHtmlPath)) {
|
|
80
|
+
logger.debug('No index.html found, skipping beacon injection');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let beaconCode: string;
|
|
85
|
+
try {
|
|
86
|
+
beaconCode = await readBeaconScript(rootDir);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logger.warn(
|
|
89
|
+
'Failed to read beacon script, skipping injection: %s',
|
|
90
|
+
error instanceof Error ? error.message : String(error)
|
|
91
|
+
);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Write beacon as a content-hashed asset (matches Vite's naming convention)
|
|
96
|
+
const hash = createHash('sha256').update(beaconCode).digest('hex').slice(0, 8);
|
|
97
|
+
const beaconFileName = `agentuity-beacon-${hash}.js`;
|
|
98
|
+
const assetsDir = join(clientDir, 'assets');
|
|
99
|
+
mkdirSync(assetsDir, { recursive: true });
|
|
100
|
+
writeFileSync(join(assetsDir, beaconFileName), beaconCode);
|
|
101
|
+
|
|
102
|
+
// If a Vite manifest exists, add the beacon so the metadata generator
|
|
103
|
+
// includes it in the asset list. When no manifest exists, the directory
|
|
104
|
+
// scanner in metadata-generator.ts picks up assets/ directly.
|
|
105
|
+
const manifestPath = join(clientDir, '.vite', 'manifest.json');
|
|
106
|
+
if (existsSync(manifestPath)) {
|
|
107
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
108
|
+
manifest['agentuity-beacon'] = { file: `assets/${beaconFileName}` };
|
|
109
|
+
writeFileSync(manifestPath, JSON.stringify(manifest));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Build the beacon URL using the CDN base
|
|
113
|
+
const normalizedBase = cdnBaseUrl.endsWith('/') ? cdnBaseUrl : `${cdnBaseUrl}/`;
|
|
114
|
+
const beaconUrl = `${normalizedBase}assets/${beaconFileName}`;
|
|
115
|
+
|
|
116
|
+
// Inject the script tag into index.html
|
|
117
|
+
// The script must be sync (no async/defer) to patch history API before router loads.
|
|
118
|
+
// The data-agentuity-beacon attribute is the marker the runtime looks for.
|
|
119
|
+
const beaconScript = `<script data-agentuity-beacon src="${beaconUrl}"></script>`;
|
|
120
|
+
|
|
121
|
+
let html = readFileSync(indexHtmlPath, 'utf-8');
|
|
122
|
+
if (html.includes('</head>')) {
|
|
123
|
+
html = html.replace('</head>', `${beaconScript}</head>`);
|
|
124
|
+
} else if (html.includes('<body')) {
|
|
125
|
+
html = html.replace(/<body([^>]*)>/, `<body$1>${beaconScript}`);
|
|
126
|
+
} else {
|
|
127
|
+
html = beaconScript + html;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
writeFileSync(indexHtmlPath, html);
|
|
131
|
+
logger.debug('Injected analytics beacon: %s', beaconUrl);
|
|
132
|
+
}
|
|
133
|
+
|
|
13
134
|
export interface ViteBuildOptions {
|
|
14
135
|
rootDir: string;
|
|
15
136
|
mode: 'client' | 'server' | 'workbench';
|
|
@@ -116,18 +237,43 @@ export default defineConfig({
|
|
|
116
237
|
await Bun.write(viteConfigPath, fallbackConfig);
|
|
117
238
|
}
|
|
118
239
|
|
|
240
|
+
// Construct CDN base URL for production builds so Vite prefixes all
|
|
241
|
+
// asset URLs (CSS, JS chunks) with the CDN origin instead of "/".
|
|
242
|
+
const cdnBaseUrl =
|
|
243
|
+
!dev && options.deploymentId
|
|
244
|
+
? `https://${options.region === 'local' ? 'localstack-static-assets.t3.storageapi.dev' : 'cdn.agentuity.com'}/${options.deploymentId}/client/`
|
|
245
|
+
: undefined;
|
|
246
|
+
|
|
247
|
+
const args = [
|
|
248
|
+
'bun',
|
|
249
|
+
'x',
|
|
250
|
+
'vite',
|
|
251
|
+
'build',
|
|
252
|
+
'--mode',
|
|
253
|
+
buildMode,
|
|
254
|
+
'--outDir',
|
|
255
|
+
clientOutDir,
|
|
256
|
+
'--logLevel',
|
|
257
|
+
'error',
|
|
258
|
+
'--clearScreen',
|
|
259
|
+
'false',
|
|
260
|
+
];
|
|
261
|
+
if (cdnBaseUrl) {
|
|
262
|
+
args.push('--base', cdnBaseUrl);
|
|
263
|
+
}
|
|
264
|
+
|
|
119
265
|
logger.debug('Spawning vite build for client (subprocess mode)');
|
|
120
266
|
logger.debug(' outDir: %s', clientOutDir);
|
|
121
267
|
logger.debug(' mode: %s', buildMode);
|
|
268
|
+
if (cdnBaseUrl) {
|
|
269
|
+
logger.debug(' base (CDN): %s', cdnBaseUrl);
|
|
270
|
+
}
|
|
122
271
|
|
|
123
|
-
const viteProcess = Bun.spawn(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
stderr: 'inherit',
|
|
129
|
-
}
|
|
130
|
-
);
|
|
272
|
+
const viteProcess = Bun.spawn(args, {
|
|
273
|
+
cwd: rootDir,
|
|
274
|
+
stdout: 'inherit',
|
|
275
|
+
stderr: 'inherit',
|
|
276
|
+
});
|
|
131
277
|
|
|
132
278
|
const exitCode = await viteProcess.exited;
|
|
133
279
|
|
|
@@ -264,6 +410,22 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
|
|
|
264
410
|
logger.debug('Moved index.html from src/web/ to client root');
|
|
265
411
|
}
|
|
266
412
|
|
|
413
|
+
// Post-build: inject analytics beacon into the built HTML.
|
|
414
|
+
// Must run AFTER the index.html normalization above (Vite may
|
|
415
|
+
// output to src/web/index.html which gets moved to the client root).
|
|
416
|
+
const isLocalRegion = options.region === 'local';
|
|
417
|
+
const cdnDomain = isLocalRegion
|
|
418
|
+
? 'localstack-static-assets.t3.storageapi.dev'
|
|
419
|
+
: 'cdn.agentuity.com';
|
|
420
|
+
const cdnBaseUrl =
|
|
421
|
+
!dev && options.deploymentId
|
|
422
|
+
? `https://${cdnDomain}/${options.deploymentId}/client/`
|
|
423
|
+
: undefined;
|
|
424
|
+
|
|
425
|
+
if (cdnBaseUrl && analyticsEnabled) {
|
|
426
|
+
await injectBeacon(rootDir, cdnBaseUrl, logger);
|
|
427
|
+
}
|
|
428
|
+
|
|
267
429
|
result.client.included = true;
|
|
268
430
|
result.client.duration = Date.now() - started;
|
|
269
431
|
endClientDiagnostic?.();
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createCommand } from '../../../types';
|
|
3
|
+
import * as tui from '../../../tui';
|
|
4
|
+
import { createStorageAdapter, resolveMeId, parseDuration, truncate } from './util';
|
|
5
|
+
import { getCommand } from '../../../command-prefix';
|
|
6
|
+
import { isDryRunMode, outputDryRun } from '../../../explain';
|
|
7
|
+
import type { TaskPriority, TaskStatus, TaskType, BatchClosedTask } from '@agentuity/core';
|
|
8
|
+
|
|
9
|
+
const TaskCloseResponseSchema = z.object({
|
|
10
|
+
success: z.boolean().describe('Whether the operation succeeded'),
|
|
11
|
+
closed: z
|
|
12
|
+
.array(
|
|
13
|
+
z.object({
|
|
14
|
+
id: z.string().describe('Closed task ID'),
|
|
15
|
+
title: z.string().describe('Closed task title'),
|
|
16
|
+
status: z.string().describe('Task status'),
|
|
17
|
+
closed_date: z.string().optional().describe('ISO 8601 closed date'),
|
|
18
|
+
})
|
|
19
|
+
)
|
|
20
|
+
.describe('List of closed tasks'),
|
|
21
|
+
count: z.number().describe('Number of tasks closed'),
|
|
22
|
+
durationMs: z.number().describe('Operation duration in milliseconds'),
|
|
23
|
+
dryRun: z.boolean().optional().describe('Whether this was a dry run'),
|
|
24
|
+
message: z.string().optional().describe('Status message'),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const closeSubcommand = createCommand({
|
|
28
|
+
name: 'close',
|
|
29
|
+
aliases: ['done', 'complete'],
|
|
30
|
+
description: 'Close a task by ID or batch-close tasks by filter',
|
|
31
|
+
tags: ['mutating', 'slow', 'requires-auth'],
|
|
32
|
+
requires: { auth: true },
|
|
33
|
+
examples: [
|
|
34
|
+
{
|
|
35
|
+
command: getCommand('cloud task close task_abc123'),
|
|
36
|
+
description: 'Close a single task by ID',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
command: getCommand('cloud task close --status in_progress --older-than 7d'),
|
|
40
|
+
description: 'Close in-progress tasks older than 7 days',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
command: getCommand('cloud task close --status open --limit 10 --dry-run'),
|
|
44
|
+
description: 'Preview which open tasks would be closed (dry run)',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
command: getCommand('cloud task close --created-id me --confirm'),
|
|
48
|
+
description: 'Close all tasks created by me without confirmation prompt',
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
schema: {
|
|
52
|
+
args: z.object({
|
|
53
|
+
id: z.string().optional().describe('Task ID to close (for single close)'),
|
|
54
|
+
}),
|
|
55
|
+
options: z.object({
|
|
56
|
+
status: z
|
|
57
|
+
.enum(['open', 'in_progress', 'started', 'done', 'completed', 'closed', 'cancelled'])
|
|
58
|
+
.optional()
|
|
59
|
+
.describe('filter batch close by status'),
|
|
60
|
+
type: z
|
|
61
|
+
.enum(['epic', 'feature', 'enhancement', 'bug', 'task'])
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('filter batch close by type'),
|
|
64
|
+
priority: z
|
|
65
|
+
.enum(['high', 'medium', 'low', 'none'])
|
|
66
|
+
.optional()
|
|
67
|
+
.describe('filter batch close by priority'),
|
|
68
|
+
olderThan: z
|
|
69
|
+
.string()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe('filter batch close by age (e.g. 30s, 7d, 24h, 2w)'),
|
|
72
|
+
parentId: z.string().optional().describe('filter batch close by parent task ID'),
|
|
73
|
+
createdId: z
|
|
74
|
+
.string()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe('filter batch close by creator ID (use "me" for current user)'),
|
|
77
|
+
assignedId: z.string().optional().describe('filter batch close by assigned user ID'),
|
|
78
|
+
projectId: z.string().optional().describe('filter batch close by project ID'),
|
|
79
|
+
tagId: z.string().optional().describe('filter batch close by tag ID'),
|
|
80
|
+
idsFile: z.string().optional().describe('path to JSON file containing task IDs to close'),
|
|
81
|
+
orgId: z.string().optional().describe('organization ID (uses default if not specified)'),
|
|
82
|
+
dryRun: z
|
|
83
|
+
.boolean()
|
|
84
|
+
.optional()
|
|
85
|
+
.default(false)
|
|
86
|
+
.describe('preview changes without executing'),
|
|
87
|
+
limit: z.coerce
|
|
88
|
+
.number()
|
|
89
|
+
.int()
|
|
90
|
+
.min(1)
|
|
91
|
+
.max(200)
|
|
92
|
+
.default(50)
|
|
93
|
+
.describe('max tasks to close in batch mode (default: 50, max: 200)'),
|
|
94
|
+
confirm: z.boolean().optional().default(false).describe('skip confirmation prompt'),
|
|
95
|
+
}),
|
|
96
|
+
response: TaskCloseResponseSchema,
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async handler(ctx) {
|
|
100
|
+
const { args, opts, options } = ctx;
|
|
101
|
+
const started = Date.now();
|
|
102
|
+
const storage = await createStorageAdapter(ctx);
|
|
103
|
+
|
|
104
|
+
const isSingleClose = !!args.id;
|
|
105
|
+
const hasFilters =
|
|
106
|
+
opts.status ||
|
|
107
|
+
opts.type ||
|
|
108
|
+
opts.priority ||
|
|
109
|
+
opts.olderThan ||
|
|
110
|
+
opts.parentId ||
|
|
111
|
+
opts.createdId ||
|
|
112
|
+
opts.assignedId ||
|
|
113
|
+
opts.projectId ||
|
|
114
|
+
opts.tagId ||
|
|
115
|
+
opts.idsFile;
|
|
116
|
+
|
|
117
|
+
if (!isSingleClose && !hasFilters) {
|
|
118
|
+
tui.fatal(
|
|
119
|
+
'Provide a task ID for single close, or use --status, --type, --priority, --older-than, --parent-id, --created-id, --assigned-id, --project-id, --tag-id, or --ids-file for batch close.'
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (isSingleClose && hasFilters) {
|
|
124
|
+
tui.fatal(
|
|
125
|
+
'Cannot combine task ID with filter options. Use either single close (by ID) or batch close (by filters).'
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (isSingleClose) {
|
|
130
|
+
if (isDryRunMode(options)) {
|
|
131
|
+
outputDryRun(`Would close task: ${args.id}`, options);
|
|
132
|
+
return {
|
|
133
|
+
success: true,
|
|
134
|
+
closed: [{ id: args.id!, title: '(dry run)', status: 'done' }],
|
|
135
|
+
count: 1,
|
|
136
|
+
durationMs: Date.now() - started,
|
|
137
|
+
dryRun: true,
|
|
138
|
+
message: 'Dry run — no tasks were closed',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!opts.confirm) {
|
|
143
|
+
const confirmed = await tui.confirm(`Close task "${args.id}"?`, false);
|
|
144
|
+
if (!confirmed) {
|
|
145
|
+
if (!options.json) tui.info('Cancelled');
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
closed: [],
|
|
149
|
+
count: 0,
|
|
150
|
+
durationMs: Date.now() - started,
|
|
151
|
+
message: 'Cancelled',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const task = await storage.close(args.id!);
|
|
157
|
+
const durationMs = Date.now() - started;
|
|
158
|
+
|
|
159
|
+
if (!options.json) {
|
|
160
|
+
tui.success(`Closed task ${tui.bold(task.id)} (${task.title}) in ${durationMs}ms`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
success: true,
|
|
165
|
+
closed: [
|
|
166
|
+
{
|
|
167
|
+
id: task.id,
|
|
168
|
+
title: task.title,
|
|
169
|
+
status: task.status,
|
|
170
|
+
closed_date: task.closed_date,
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
count: 1,
|
|
174
|
+
durationMs,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Batch close mode
|
|
179
|
+
if (opts.olderThan) {
|
|
180
|
+
parseDuration(opts.olderThan);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const createdId = resolveMeId(opts.createdId, ctx);
|
|
184
|
+
const assignedId = resolveMeId(opts.assignedId, ctx);
|
|
185
|
+
|
|
186
|
+
// Handle IDs file
|
|
187
|
+
let explicitIds: string[] | undefined;
|
|
188
|
+
if (opts.idsFile) {
|
|
189
|
+
const file = Bun.file(opts.idsFile);
|
|
190
|
+
if (!(await file.exists())) {
|
|
191
|
+
tui.fatal(`IDs file not found: ${opts.idsFile}`);
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const content = await file.json();
|
|
195
|
+
if (Array.isArray(content)) {
|
|
196
|
+
explicitIds = content.map((id) => String(id));
|
|
197
|
+
} else if (content && Array.isArray((content as { ids?: string[] }).ids)) {
|
|
198
|
+
explicitIds = (content as { ids: string[] }).ids;
|
|
199
|
+
} else {
|
|
200
|
+
tui.fatal(`Invalid IDs file format. Expected array of IDs or { ids: [...] }`);
|
|
201
|
+
}
|
|
202
|
+
} catch (err) {
|
|
203
|
+
tui.fatal(`Failed to parse IDs file: ${err}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const batchParams = {
|
|
208
|
+
status: opts.status as TaskStatus | undefined,
|
|
209
|
+
type: opts.type as TaskType | undefined,
|
|
210
|
+
priority: opts.priority as TaskPriority | undefined,
|
|
211
|
+
parent_id: opts.parentId,
|
|
212
|
+
created_id: createdId,
|
|
213
|
+
assigned_id: assignedId,
|
|
214
|
+
project_id: opts.projectId,
|
|
215
|
+
tag_id: opts.tagId,
|
|
216
|
+
older_than: opts.olderThan,
|
|
217
|
+
ids: explicitIds,
|
|
218
|
+
limit: opts.limit,
|
|
219
|
+
closed_id: ctx.auth.userId,
|
|
220
|
+
dry_run: isDryRunMode(options),
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// For confirmation, run a dry-run first to preview
|
|
224
|
+
if (!isDryRunMode(options) && !opts.confirm) {
|
|
225
|
+
const preview = await storage.batchClose({ ...batchParams, dry_run: true });
|
|
226
|
+
|
|
227
|
+
if (preview.count === 0) {
|
|
228
|
+
if (!options.json) tui.info('No tasks match the given filters');
|
|
229
|
+
return {
|
|
230
|
+
success: true,
|
|
231
|
+
closed: [],
|
|
232
|
+
count: 0,
|
|
233
|
+
durationMs: Date.now() - started,
|
|
234
|
+
message: 'No matching tasks found',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!options.json) {
|
|
239
|
+
tui.warning(
|
|
240
|
+
`Found ${preview.count} ${tui.plural(preview.count, 'task', 'tasks')} to close:`
|
|
241
|
+
);
|
|
242
|
+
tui.newline();
|
|
243
|
+
|
|
244
|
+
const tableData = preview.closed.map((task: BatchClosedTask) => ({
|
|
245
|
+
ID: tui.muted(truncate(task.id, 28)),
|
|
246
|
+
Title: truncate(task.title, 40),
|
|
247
|
+
Status: task.status,
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
tui.table(tableData, [
|
|
251
|
+
{ name: 'ID', alignment: 'left' },
|
|
252
|
+
{ name: 'Title', alignment: 'left' },
|
|
253
|
+
{ name: 'Status', alignment: 'left' },
|
|
254
|
+
]);
|
|
255
|
+
tui.newline();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const confirmed = await tui.confirm(
|
|
259
|
+
`Close ${preview.count} ${tui.plural(preview.count, 'task', 'tasks')}?`,
|
|
260
|
+
false
|
|
261
|
+
);
|
|
262
|
+
if (!confirmed) {
|
|
263
|
+
if (!options.json) tui.info('Cancelled');
|
|
264
|
+
return {
|
|
265
|
+
success: false,
|
|
266
|
+
closed: [],
|
|
267
|
+
count: 0,
|
|
268
|
+
durationMs: Date.now() - started,
|
|
269
|
+
message: 'Cancelled',
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Execute batch close
|
|
275
|
+
const result = await storage.batchClose(batchParams);
|
|
276
|
+
const durationMs = Date.now() - started;
|
|
277
|
+
|
|
278
|
+
if (!options.json) {
|
|
279
|
+
if (result.dry_run) {
|
|
280
|
+
if (result.count > 0) {
|
|
281
|
+
tui.info(
|
|
282
|
+
`Dry run: would close ${result.count} ${tui.plural(result.count, 'task', 'tasks')}`
|
|
283
|
+
);
|
|
284
|
+
} else {
|
|
285
|
+
tui.info('No tasks match the given filters');
|
|
286
|
+
}
|
|
287
|
+
} else if (result.count > 0) {
|
|
288
|
+
tui.success(
|
|
289
|
+
`Closed ${result.count} ${tui.plural(result.count, 'task', 'tasks')} in ${durationMs}ms`
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// Show which tasks were closed
|
|
293
|
+
if (result.closed.length > 0) {
|
|
294
|
+
tui.newline();
|
|
295
|
+
const closedTable = result.closed.map((task) => ({
|
|
296
|
+
ID: tui.muted(truncate(task.id, 28)),
|
|
297
|
+
Title: truncate(task.title, 40),
|
|
298
|
+
}));
|
|
299
|
+
tui.table(closedTable, [
|
|
300
|
+
{ name: 'ID', alignment: 'left' },
|
|
301
|
+
{ name: 'Title', alignment: 'left' },
|
|
302
|
+
]);
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
tui.info('No tasks matched the given filters');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
success: true,
|
|
311
|
+
closed: result.closed,
|
|
312
|
+
count: result.count,
|
|
313
|
+
durationMs,
|
|
314
|
+
dryRun: result.dry_run,
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
export default closeSubcommand;
|
|
@@ -1,46 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { createCommand } from '../../../types';
|
|
3
3
|
import * as tui from '../../../tui';
|
|
4
|
-
import { createStorageAdapter } from './util';
|
|
4
|
+
import { createStorageAdapter, resolveMeId, parseDuration, truncate } from './util';
|
|
5
5
|
import { getCommand } from '../../../command-prefix';
|
|
6
6
|
import { isDryRunMode, outputDryRun } from '../../../explain';
|
|
7
7
|
import type { TaskPriority, TaskStatus, TaskType, BatchDeletedTask } from '@agentuity/core';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
}
|
|
9
|
+
// Re-export for testing
|
|
10
|
+
export { parseDuration } from './util';
|
|
44
11
|
|
|
45
12
|
const TaskDeleteResponseSchema = z.object({
|
|
46
13
|
success: z.boolean().describe('Whether the operation succeeded'),
|
|
@@ -104,7 +71,16 @@ export const deleteSubcommand = createCommand({
|
|
|
104
71
|
.optional()
|
|
105
72
|
.describe('filter batch delete by age (e.g. 30s, 7d, 24h, 2w)'),
|
|
106
73
|
parentId: z.string().optional().describe('filter batch delete by parent task ID'),
|
|
107
|
-
createdId: z
|
|
74
|
+
createdId: z
|
|
75
|
+
.string()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe('filter batch delete by creator ID (use "me" for current user)'),
|
|
78
|
+
orgId: z.string().optional().describe('organization ID (uses default if not specified)'),
|
|
79
|
+
dryRun: z
|
|
80
|
+
.boolean()
|
|
81
|
+
.optional()
|
|
82
|
+
.default(false)
|
|
83
|
+
.describe('preview changes without executing'),
|
|
108
84
|
limit: z.coerce
|
|
109
85
|
.number()
|
|
110
86
|
.int()
|
|
@@ -198,7 +174,7 @@ export const deleteSubcommand = createCommand({
|
|
|
198
174
|
type: opts.type as TaskType | undefined,
|
|
199
175
|
priority: opts.priority as TaskPriority | undefined,
|
|
200
176
|
parent_id: opts.parentId,
|
|
201
|
-
created_id: opts.createdId,
|
|
177
|
+
created_id: resolveMeId(opts.createdId, ctx),
|
|
202
178
|
older_than: opts.olderThan,
|
|
203
179
|
limit: opts.limit,
|
|
204
180
|
};
|
|
@@ -212,6 +188,7 @@ export const deleteSubcommand = createCommand({
|
|
|
212
188
|
type: batchParams.type,
|
|
213
189
|
priority: batchParams.priority,
|
|
214
190
|
parent_id: batchParams.parent_id,
|
|
191
|
+
created_id: batchParams.created_id,
|
|
215
192
|
limit: batchParams.limit,
|
|
216
193
|
sort: 'created_at',
|
|
217
194
|
order: 'asc',
|
|
@@ -219,11 +196,6 @@ export const deleteSubcommand = createCommand({
|
|
|
219
196
|
|
|
220
197
|
// Client-side filters for preview (server will apply these on actual delete)
|
|
221
198
|
let candidates = preview.tasks;
|
|
222
|
-
if (batchParams.created_id) {
|
|
223
|
-
candidates = candidates.filter(
|
|
224
|
-
(t: { created_id: string }) => t.created_id === batchParams.created_id
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
199
|
if (opts.olderThan) {
|
|
228
200
|
const durationMs = parseDuration(opts.olderThan);
|
|
229
201
|
const cutoff = new Date(Date.now() - durationMs);
|
|
@@ -4,6 +4,7 @@ import { createSubcommand } from './create';
|
|
|
4
4
|
import { updateSubcommand } from './update';
|
|
5
5
|
import { listSubcommand } from './list';
|
|
6
6
|
import { deleteSubcommand } from './delete';
|
|
7
|
+
import { closeSubcommand } from './close';
|
|
7
8
|
import { statsSubcommand } from './stats';
|
|
8
9
|
import { attachmentSubcommand } from './attachment';
|
|
9
10
|
import { userSubcommand } from './user';
|
|
@@ -36,6 +37,14 @@ export const taskCommand = createCommand({
|
|
|
36
37
|
command: getCommand('cloud task delete task_abc123'),
|
|
37
38
|
description: 'Delete a task by ID',
|
|
38
39
|
},
|
|
40
|
+
{
|
|
41
|
+
command: getCommand('cloud task close task_abc123'),
|
|
42
|
+
description: 'Close a task by ID',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
command: getCommand('cloud task close --status done --older-than 7d'),
|
|
46
|
+
description: 'Batch close done tasks older than 7 days',
|
|
47
|
+
},
|
|
39
48
|
{
|
|
40
49
|
command: getCommand('cloud task delete --status done --older-than 7d'),
|
|
41
50
|
description: 'Batch delete done tasks older than 7 days',
|
|
@@ -59,6 +68,7 @@ export const taskCommand = createCommand({
|
|
|
59
68
|
updateSubcommand,
|
|
60
69
|
listSubcommand,
|
|
61
70
|
deleteSubcommand,
|
|
71
|
+
closeSubcommand,
|
|
62
72
|
statsSubcommand,
|
|
63
73
|
attachmentSubcommand,
|
|
64
74
|
userSubcommand,
|