@zalosai/zalos-cli 0.2.13

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.
@@ -0,0 +1,34 @@
1
+ import { resolveEnvironment } from '../environments.js';
2
+ import { runDatabaseTool } from '../utils/developer-db.js';
3
+ import { handleError } from '../utils/errors.js';
4
+ import { printJson } from '../utils/output.js';
5
+ import { doctorReport } from './doctor.js';
6
+ export async function runSetup(environment, deps) {
7
+ resolveEnvironment({ flag: environment });
8
+ await deps.run(['setup']);
9
+ await deps.login();
10
+ return deps.doctor(environment);
11
+ }
12
+ export function registerSetupCommand(program) {
13
+ program
14
+ .command('setup')
15
+ .description('Configure database access, sign in, and check access')
16
+ .action(async () => {
17
+ try {
18
+ const environment = resolveEnvironment({ env: process.env.ZL_ENVIRONMENT }).name;
19
+ const { loginAction } = await import('./auth.js');
20
+ const result = await runSetup(environment, {
21
+ run: runDatabaseTool,
22
+ login: () => loginAction(),
23
+ doctor: doctorReport,
24
+ });
25
+ printJson(result);
26
+ if (!result.ok)
27
+ process.exitCode = 1;
28
+ }
29
+ catch (error) {
30
+ handleError(error);
31
+ }
32
+ });
33
+ }
34
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1,30 @@
1
+ import { client } from '../client.js';
2
+ import { handleError } from '../utils/errors.js';
3
+ import { printList } from '../utils/output.js';
4
+ import { validateId } from '../utils/validate.js';
5
+ export async function listStatesAction(options) {
6
+ validateId(options.projectId, 'projectId');
7
+ const query = {};
8
+ if (options.cursor)
9
+ query['cursor'] = options.cursor;
10
+ const response = await client.get(`/projects/${options.projectId}/states`, Object.keys(query).length > 0 ? query : undefined);
11
+ printList(response.data, options.fields, response.nextCursor);
12
+ }
13
+ export function registerStateCommand(program) {
14
+ const state = program.command('state').description('Manage task states');
15
+ state
16
+ .command('list')
17
+ .description('List states for a project')
18
+ .requiredOption('--project-id <id>', 'Project ID')
19
+ .option('--cursor <token>', 'Pagination cursor from previous response')
20
+ .action(async (options) => {
21
+ try {
22
+ const globalOpts = program.optsWithGlobals();
23
+ await listStatesAction({ ...options, fields: globalOpts.fields });
24
+ }
25
+ catch (error) {
26
+ handleError(error);
27
+ }
28
+ });
29
+ }
30
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1,75 @@
1
+ import { client } from '../client.js';
2
+ import { handleError, ValidationError } from '../utils/errors.js';
3
+ import { printJson, printList } from '../utils/output.js';
4
+ import { readStdin } from '../utils/stdin.js';
5
+ import { validateId, validateString } from '../utils/validate.js';
6
+ export async function listCommentsAction(taskId, options) {
7
+ validateId(taskId, 'taskId');
8
+ const query = {};
9
+ if (options.cursor)
10
+ query['cursor'] = options.cursor;
11
+ const comments = await client.get(`/tasks/${taskId}/comments`, Object.keys(query).length > 0 ? query : undefined);
12
+ printList(comments.data, options.fields, comments.nextCursor);
13
+ }
14
+ export async function addCommentAction(taskId, options) {
15
+ validateId(taskId, 'taskId');
16
+ let body;
17
+ if (options.body === '-') {
18
+ if (process.stdin.isTTY) {
19
+ throw new ValidationError('body', 'Comment body is required — pipe input or pass --body <text>');
20
+ }
21
+ body = (await readStdin()).trim();
22
+ if (!body)
23
+ throw new ValidationError('body', 'Comment body is required (got empty input from stdin)');
24
+ }
25
+ else {
26
+ body = options.body;
27
+ if (!body)
28
+ throw new ValidationError('body', 'Comment body is required (body cannot be empty)');
29
+ }
30
+ validateString(body, 'body');
31
+ if (options.dryRun) {
32
+ printJson({
33
+ dryRun: true,
34
+ method: 'POST',
35
+ path: `/tasks/${taskId}/comments`,
36
+ payload: { body },
37
+ });
38
+ return;
39
+ }
40
+ const result = await client.post(`/tasks/${taskId}/comments`, { body });
41
+ printJson(result);
42
+ }
43
+ export function registerTaskCommentCommand(taskCommand, program) {
44
+ const comment = taskCommand.command('comment').description('Manage task comments');
45
+ comment
46
+ .command('list <task-id>')
47
+ .description('List comments on a task')
48
+ .option('--cursor <token>', 'Pagination cursor from previous response')
49
+ .action(async (taskId, options) => {
50
+ try {
51
+ const globalOpts = program.optsWithGlobals();
52
+ await listCommentsAction(taskId, { fields: globalOpts.fields, cursor: options.cursor });
53
+ }
54
+ catch (error) {
55
+ handleError(error);
56
+ }
57
+ });
58
+ comment
59
+ .command('add <task-id>')
60
+ .description('Add a comment to a task')
61
+ .option('--body <text>', 'Comment body; use - to read from stdin')
62
+ .option('--dry-run', 'Preview without adding', false)
63
+ .action(async (taskId, options) => {
64
+ try {
65
+ if (options.body === undefined) {
66
+ handleError(new ValidationError('body', 'Comment body is required (--body <text> or --body -)'));
67
+ }
68
+ await addCommentAction(taskId, { body: options.body, dryRun: options.dryRun });
69
+ }
70
+ catch (error) {
71
+ handleError(error);
72
+ }
73
+ });
74
+ }
75
+ //# sourceMappingURL=task-comment.js.map
@@ -0,0 +1,245 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { basename, extname } from 'node:path';
3
+ import { client } from '../client.js';
4
+ import { handleError, ValidationError } from '../utils/errors.js';
5
+ import { filterSingle, printJson, printList } from '../utils/output.js';
6
+ import { parseJson, validateId } from '../utils/validate.js';
7
+ const IMAGE_MIME_TYPES = {
8
+ '.avif': 'image/avif',
9
+ '.gif': 'image/gif',
10
+ '.jpeg': 'image/jpeg',
11
+ '.jpg': 'image/jpeg',
12
+ '.png': 'image/png',
13
+ '.svg': 'image/svg+xml',
14
+ '.webp': 'image/webp',
15
+ };
16
+ const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
17
+ const TASK_PRIORITIES = ['critical', 'high', 'medium', 'low'];
18
+ function isRecord(value) {
19
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
20
+ }
21
+ export async function listTasksAction(options) {
22
+ validateId(options.projectId, 'projectId');
23
+ const limit = Number.parseInt(options.limit ?? '20', 10);
24
+ if (!Number.isInteger(limit) ||
25
+ String(limit) !== (options.limit ?? '20') ||
26
+ limit < 1 ||
27
+ limit > 100) {
28
+ throw new ValidationError('--limit', '--limit must be an integer between 1 and 100');
29
+ }
30
+ if (options.priority &&
31
+ !TASK_PRIORITIES.includes(options.priority)) {
32
+ throw new ValidationError('--priority', `--priority must be one of: ${TASK_PRIORITIES.join(', ')}`);
33
+ }
34
+ const query = {};
35
+ if (options.stateId)
36
+ query['stateId'] = options.stateId;
37
+ if (options.assigneeId)
38
+ query['assigneeId'] = options.assigneeId;
39
+ if (options.priority)
40
+ query['priority'] = options.priority;
41
+ if (options.limit)
42
+ query['limit'] = options.limit;
43
+ if (options.cursor)
44
+ query['cursor'] = options.cursor;
45
+ const response = await client.get(`/projects/${options.projectId}/tasks`, Object.keys(query).length > 0 ? query : undefined);
46
+ const filtered = response.data.filter((task) => (!options.stateId || task['stateId'] === options.stateId) &&
47
+ (!options.assigneeId || task['assigneeId'] === options.assigneeId) &&
48
+ (!options.priority || task['priority'] === options.priority));
49
+ // Current production returns the complete project task set and ignores list query parameters.
50
+ // Preserve server pagination when it is available, otherwise enforce the CLI contract locally.
51
+ if (response.nextCursor !== undefined) {
52
+ printList(filtered, options.fields, response.nextCursor);
53
+ return;
54
+ }
55
+ let startIndex = 0;
56
+ if (options.cursor) {
57
+ const cursorIndex = filtered.findIndex((task) => task['id'] === options.cursor);
58
+ if (cursorIndex === -1) {
59
+ throw new ValidationError('--cursor', 'Cursor does not match a task in this result set');
60
+ }
61
+ startIndex = cursorIndex + 1;
62
+ }
63
+ const page = filtered.slice(startIndex, startIndex + limit);
64
+ const hasMore = startIndex + page.length < filtered.length;
65
+ const nextCursor = hasMore ? String(page.at(-1)?.['id']) : undefined;
66
+ printList(page, options.fields, nextCursor);
67
+ }
68
+ export async function viewTaskAction(taskId, options) {
69
+ validateId(taskId, 'taskId');
70
+ const response = await client.get(`/tasks/${taskId}`);
71
+ const task = 'data' in response && isRecord(response.data) ? response.data : response;
72
+ printJson(filterSingle(task, options.fields));
73
+ }
74
+ export async function createTaskAction(options) {
75
+ validateId(options.projectId, 'projectId');
76
+ const payload = parseJson(options.json);
77
+ if (options.dryRun) {
78
+ printJson({
79
+ dryRun: true,
80
+ method: 'POST',
81
+ path: `/projects/${options.projectId}/tasks`,
82
+ payload,
83
+ });
84
+ return;
85
+ }
86
+ const result = await client.post(`/projects/${options.projectId}/tasks`, payload);
87
+ printJson(result);
88
+ }
89
+ export async function updateTaskAction(taskId, options) {
90
+ validateId(taskId, 'taskId');
91
+ const payload = parseJson(options.json);
92
+ if (options.dryRun) {
93
+ printJson({ dryRun: true, method: 'PATCH', path: `/tasks/${taskId}`, payload });
94
+ return;
95
+ }
96
+ const result = await client.patch(`/tasks/${taskId}`, payload);
97
+ printJson(result);
98
+ }
99
+ export async function deleteTaskAction(taskId, options) {
100
+ validateId(taskId, 'taskId');
101
+ if (options.dryRun) {
102
+ printJson({ dryRun: true, method: 'DELETE', path: `/tasks/${taskId}` });
103
+ return;
104
+ }
105
+ await client.delete(`/tasks/${taskId}`);
106
+ printJson({ deleted: true, taskId });
107
+ }
108
+ export async function moveTaskAction(taskId, options) {
109
+ validateId(taskId, 'taskId');
110
+ validateId(options.projectId, 'projectId');
111
+ const payload = options.stateName?.trim() ? { stateName: options.stateName.trim() } : {};
112
+ const path = `/projects/${options.projectId}/tasks/${taskId}/move`;
113
+ if (options.dryRun) {
114
+ printJson({ dryRun: true, method: 'POST', path, payload });
115
+ return;
116
+ }
117
+ printJson(await client.post(path, payload));
118
+ }
119
+ export async function uploadTaskAttachmentAction(taskId, filePath) {
120
+ validateId(taskId, 'taskId');
121
+ const mimeType = IMAGE_MIME_TYPES[extname(filePath).toLowerCase()];
122
+ if (!mimeType) {
123
+ throw new ValidationError('file', 'Only image files are supported');
124
+ }
125
+ let fileSize;
126
+ try {
127
+ fileSize = (await stat(filePath)).size;
128
+ }
129
+ catch {
130
+ throw new ValidationError('file', `Unable to read image file: ${filePath}`);
131
+ }
132
+ if (fileSize > MAX_IMAGE_BYTES) {
133
+ throw new ValidationError('file', 'Image too large — max 10 MB');
134
+ }
135
+ let contents;
136
+ try {
137
+ contents = await readFile(filePath);
138
+ }
139
+ catch {
140
+ throw new ValidationError('file', `Unable to read image file: ${filePath}`);
141
+ }
142
+ const form = new FormData();
143
+ const imageBytes = new Uint8Array(contents);
144
+ form.append('file', new Blob([imageBytes], { type: mimeType }), basename(filePath));
145
+ printJson(await client.postForm(`/tasks/${taskId}/attachments`, form));
146
+ }
147
+ export function registerTaskCommand(program) {
148
+ const task = program.command('task').description('Manage tasks');
149
+ task
150
+ .command('list')
151
+ .description('List tasks in a project')
152
+ .requiredOption('--project-id <id>', 'Project ID')
153
+ .option('--state-id <id>', 'Filter by state ID')
154
+ .option('--assignee-id <id>', 'Filter by assignee ID')
155
+ .option('--priority <level>', 'Filter by priority: critical|high|medium|low')
156
+ .option('--limit <n>', 'Max results (default 20, max 100)', '20')
157
+ .option('--cursor <token>', 'Pagination cursor from previous response')
158
+ .action(async (options) => {
159
+ try {
160
+ const globalOpts = program.optsWithGlobals();
161
+ await listTasksAction({ ...options, fields: globalOpts.fields });
162
+ }
163
+ catch (error) {
164
+ handleError(error);
165
+ }
166
+ });
167
+ task
168
+ .command('view <task-id>')
169
+ .description('View a task by ID')
170
+ .action(async (taskId) => {
171
+ try {
172
+ const globalOpts = program.optsWithGlobals();
173
+ await viewTaskAction(taskId, { fields: globalOpts.fields });
174
+ }
175
+ catch (error) {
176
+ handleError(error);
177
+ }
178
+ });
179
+ task
180
+ .command('create')
181
+ .description('Create a task')
182
+ .requiredOption('--project-id <id>', 'Project ID')
183
+ .requiredOption('--json <payload>', 'Task fields as JSON (see: zl schema task)')
184
+ .option('--dry-run', 'Preview without creating', false)
185
+ .action(async (options) => {
186
+ try {
187
+ await createTaskAction(options);
188
+ }
189
+ catch (error) {
190
+ handleError(error);
191
+ }
192
+ });
193
+ task
194
+ .command('update <task-id>')
195
+ .description('Update a task')
196
+ .requiredOption('--json <payload>', 'Fields to update as JSON (see: zl schema task)')
197
+ .option('--dry-run', 'Preview without updating', false)
198
+ .action(async (taskId, options) => {
199
+ try {
200
+ await updateTaskAction(taskId, options);
201
+ }
202
+ catch (error) {
203
+ handleError(error);
204
+ }
205
+ });
206
+ task
207
+ .command('delete <task-id>')
208
+ .description('Delete a task')
209
+ .option('--dry-run', 'Preview without deleting', false)
210
+ .action(async (taskId, options) => {
211
+ try {
212
+ await deleteTaskAction(taskId, options);
213
+ }
214
+ catch (error) {
215
+ handleError(error);
216
+ }
217
+ });
218
+ task
219
+ .command('move <task-id>')
220
+ .description('Move a task to another project')
221
+ .requiredOption('--project-id <id>', 'Target project ID')
222
+ .option('--state-name <name>', 'Target state name (defaults to an equivalent state)')
223
+ .option('--dry-run', 'Preview without moving', false)
224
+ .action(async (taskId, options) => {
225
+ try {
226
+ await moveTaskAction(taskId, options);
227
+ }
228
+ catch (error) {
229
+ handleError(error);
230
+ }
231
+ });
232
+ const attachment = task.command('attachment').description('Manage task image attachments');
233
+ attachment
234
+ .command('upload <task-id> <file>')
235
+ .description('Upload an image attachment to a task')
236
+ .action(async (taskId, filePath) => {
237
+ try {
238
+ await uploadTaskAttachmentAction(taskId, filePath);
239
+ }
240
+ catch (error) {
241
+ handleError(error);
242
+ }
243
+ });
244
+ }
245
+ //# sourceMappingURL=task.js.map
@@ -0,0 +1,88 @@
1
+ import { client, revokeCurrentHumanToken } from '../client.js';
2
+ import { readProfile, writeProfile } from '../credentials.js';
3
+ import { currentEnvironment } from '../environments.js';
4
+ import { AuthError } from '../utils/errors.js';
5
+ import { action, dataRow, dataRows, fields, isRow, printPage, resourceId, } from '../utils/product-api.js';
6
+ import { printJson } from '../utils/output.js';
7
+ export async function listTeamsAction(options) {
8
+ const path = options.allTeams ? '/developer/teams' : '/auth/cli-token/teams';
9
+ printPage(dataRows(await client.get(path)), options);
10
+ }
11
+ function metadata(value, name) {
12
+ if (!isRow(value) || typeof value.id !== 'string' || typeof value.name !== 'string')
13
+ throw new AuthError(`Invalid ${name} metadata returned by server`);
14
+ return {
15
+ id: value.id,
16
+ ...(typeof value.publicId === 'string' ? { publicId: value.publicId } : {}),
17
+ name: value.name,
18
+ };
19
+ }
20
+ export async function switchTeamAction(teamReference) {
21
+ if (process.env.ZL_API_TOKEN)
22
+ throw new AuthError('Team switching requires a persisted human profile; unset ZL_API_TOKEN');
23
+ const profileName = process.env.ZL_PROFILE ?? 'default';
24
+ const profile = readProfile(profileName);
25
+ if (!profile)
26
+ throw new AuthError('Not authenticated. Run: zl auth login');
27
+ const environment = currentEnvironment(profile.environment);
28
+ if (profile.apiUrl !== environment.apiUrl)
29
+ throw new AuthError('Profile API origin mismatch; log in to this origin again');
30
+ const selectedTeamReference = resourceId(teamReference);
31
+ if (profile.team?.id === selectedTeamReference ||
32
+ profile.team?.publicId === selectedTeamReference) {
33
+ printJson({
34
+ switched: false,
35
+ message: 'Team already selected',
36
+ profile: profileName,
37
+ team: profile.team,
38
+ });
39
+ return;
40
+ }
41
+ const result = dataRow(await client.post('/auth/cli-token/switch', { teamId: selectedTeamReference }, { detailedAuthErrors: true }));
42
+ if (typeof result.token !== 'string' ||
43
+ !result.token ||
44
+ typeof result.tokenId !== 'string' ||
45
+ !result.tokenId ||
46
+ typeof result.expiresAt !== 'string' ||
47
+ result.expiresAt !== profile.expiresAt)
48
+ throw new AuthError('Invalid replacement token returned by server');
49
+ const organization = metadata(result.organization, 'organization');
50
+ const team = metadata(result.team, 'team');
51
+ writeProfile(profileName, {
52
+ ...profile,
53
+ token: result.token,
54
+ tokenId: result.tokenId,
55
+ expiresAt: result.expiresAt,
56
+ organization,
57
+ team,
58
+ });
59
+ try {
60
+ await revokeCurrentHumanToken(profile.token);
61
+ }
62
+ catch {
63
+ process.stderr.write('Warning: team switched, but the previous token could not be revoked and remains valid until expiry.\n');
64
+ }
65
+ printJson({
66
+ switched: true,
67
+ profile: profileName,
68
+ environment: profile.environment,
69
+ expiresAt: result.expiresAt,
70
+ organization,
71
+ team,
72
+ });
73
+ }
74
+ export function registerTeamCommand(program) {
75
+ const command = program.command('team').description('Inspect and select regional teams');
76
+ command
77
+ .command('list')
78
+ .option('--all-teams', 'List every team in the selected region (internal human only)')
79
+ .option('--limit <n>', 'Maximum results', '100')
80
+ .option('--offset <n>', 'Result offset', '0')
81
+ .action(async (options) => action(() => listTeamsAction({ ...options, ...fields(program) })));
82
+ command
83
+ .command('switch')
84
+ .description('Switch the selected human profile to an accessible team')
85
+ .argument('<team-ref>', 'Target public ID (preferred) or UUID from zl team list')
86
+ .action(async (teamReference) => action(() => switchTeamAction(teamReference)));
87
+ }
88
+ //# sourceMappingURL=team.js.map
@@ -0,0 +1,21 @@
1
+ import { client } from '../client.js';
2
+ import { filterSingle, printJson } from '../utils/output.js';
3
+ import { action, dataRow, dataRows, fields, printPage, resourceId, } from '../utils/product-api.js';
4
+ export async function listTemplatesAction(options) {
5
+ printPage(dataRows(await client.get('/templates')), options);
6
+ }
7
+ export async function viewTemplateAction(id, options) {
8
+ printJson(filterSingle(dataRow(await client.get(`/templates/${resourceId(id)}`)), options.fields));
9
+ }
10
+ export function registerTemplateCommand(program) {
11
+ const cmd = program.command('template');
12
+ cmd
13
+ .command('list')
14
+ .option('--limit <n>', 'Maximum results', '100')
15
+ .option('--offset <n>', 'Result offset', '0')
16
+ .action(async (o) => action(() => listTemplatesAction({ ...o, ...fields(program) })));
17
+ cmd
18
+ .command('view <id>')
19
+ .action(async (id) => action(() => viewTemplateAction(id, fields(program))));
20
+ }
21
+ //# sourceMappingURL=template.js.map