@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.
- package/PUBLISHING.md +97 -0
- package/README.md +231 -0
- package/dist/client.js +79 -0
- package/dist/commands/api-token.js +74 -0
- package/dist/commands/auth.js +196 -0
- package/dist/commands/credential.js +15 -0
- package/dist/commands/cycle.js +47 -0
- package/dist/commands/doctor.js +106 -0
- package/dist/commands/execution.js +117 -0
- package/dist/commands/file.js +257 -0
- package/dist/commands/label.js +30 -0
- package/dist/commands/member.js +23 -0
- package/dist/commands/page-markdown.js +213 -0
- package/dist/commands/page.js +178 -0
- package/dist/commands/project.js +97 -0
- package/dist/commands/schema.js +178 -0
- package/dist/commands/setup.js +34 -0
- package/dist/commands/state.js +30 -0
- package/dist/commands/task-comment.js +75 -0
- package/dist/commands/task.js +245 -0
- package/dist/commands/team.js +88 -0
- package/dist/commands/template.js +21 -0
- package/dist/commands/workflow.js +245 -0
- package/dist/credentials.js +123 -0
- package/dist/environments.js +42 -0
- package/dist/main.js +86 -0
- package/dist/utils/cli-auth.js +186 -0
- package/dist/utils/developer-db.js +78 -0
- package/dist/utils/download-file.js +65 -0
- package/dist/utils/errors.js +60 -0
- package/dist/utils/output.js +30 -0
- package/dist/utils/product-api.js +48 -0
- package/dist/utils/stdin.js +23 -0
- package/dist/utils/validate.js +39 -0
- package/dist/workflow-file.js +34 -0
- package/package.json +53 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { client } from '../client.js';
|
|
2
|
+
import { filterSingle } from '../utils/output.js';
|
|
3
|
+
import { action, dataRows, fields, printPage } from '../utils/product-api.js';
|
|
4
|
+
export async function listCredentialsAction(options) {
|
|
5
|
+
printPage(dataRows(await client.get('/credentials')).map((row) => filterSingle(row, 'id,name,type,createdAt,updatedAt')), options);
|
|
6
|
+
}
|
|
7
|
+
export function registerCredentialCommand(program) {
|
|
8
|
+
const cmd = program.command('credential');
|
|
9
|
+
cmd
|
|
10
|
+
.command('list')
|
|
11
|
+
.option('--limit <n>', 'Maximum results', '100')
|
|
12
|
+
.option('--offset <n>', 'Result offset', '0')
|
|
13
|
+
.action(async (o) => action(() => listCredentialsAction({ ...o, ...fields(program) })));
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=credential.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { client } from '../client.js';
|
|
2
|
+
import { handleError } from '../utils/errors.js';
|
|
3
|
+
import { filterSingle, printJson, printList } from '../utils/output.js';
|
|
4
|
+
import { validateId } from '../utils/validate.js';
|
|
5
|
+
export async function listCyclesAction(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}/cycles`, Object.keys(query).length > 0 ? query : undefined);
|
|
11
|
+
printList(response.data, options.fields, response.nextCursor);
|
|
12
|
+
}
|
|
13
|
+
export async function viewCycleAction(cycleId, options) {
|
|
14
|
+
validateId(cycleId, 'cycleId');
|
|
15
|
+
const cycle = await client.get(`/cycles/${cycleId}`);
|
|
16
|
+
printJson(filterSingle(cycle, options.fields));
|
|
17
|
+
}
|
|
18
|
+
export function registerCycleCommand(program) {
|
|
19
|
+
const cycle = program.command('cycle').description('Manage cycles');
|
|
20
|
+
cycle
|
|
21
|
+
.command('list')
|
|
22
|
+
.description('List cycles for a project')
|
|
23
|
+
.requiredOption('--project-id <id>', 'Project ID')
|
|
24
|
+
.option('--cursor <token>', 'Pagination cursor from previous response')
|
|
25
|
+
.action(async (options) => {
|
|
26
|
+
try {
|
|
27
|
+
const globalOpts = program.optsWithGlobals();
|
|
28
|
+
await listCyclesAction({ ...options, fields: globalOpts.fields });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
handleError(error);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
cycle
|
|
35
|
+
.command('view <cycle-id>')
|
|
36
|
+
.description('View a cycle by ID')
|
|
37
|
+
.action(async (cycleId) => {
|
|
38
|
+
try {
|
|
39
|
+
const globalOpts = program.optsWithGlobals();
|
|
40
|
+
await viewCycleAction(cycleId, { fields: globalOpts.fields });
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
handleError(error);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=cycle.js.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { currentEnvironment, resolveEnvironment } from '../environments.js';
|
|
3
|
+
import { DATABASE_INSTALL, DatabaseToolError, runDatabaseTool } from '../utils/developer-db.js';
|
|
4
|
+
import { handleError } from '../utils/errors.js';
|
|
5
|
+
import { printJson } from '../utils/output.js';
|
|
6
|
+
function object(value) {
|
|
7
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
8
|
+
? value
|
|
9
|
+
: {};
|
|
10
|
+
}
|
|
11
|
+
function metadata(value, fields) {
|
|
12
|
+
const source = object(value);
|
|
13
|
+
return Object.fromEntries(fields
|
|
14
|
+
.filter((key) => typeof source[key] === 'string' ||
|
|
15
|
+
typeof source[key] === 'number' ||
|
|
16
|
+
source[key] === null)
|
|
17
|
+
.map((key) => [key, source[key]]));
|
|
18
|
+
}
|
|
19
|
+
export async function buildDoctorReport(environment, deps) {
|
|
20
|
+
const selected = currentEnvironment(undefined, environment);
|
|
21
|
+
let api;
|
|
22
|
+
try {
|
|
23
|
+
const status = await deps.authStatus();
|
|
24
|
+
api =
|
|
25
|
+
status.environment !== environment
|
|
26
|
+
? { ok: false, code: 'ENVIRONMENT_MISMATCH' }
|
|
27
|
+
: {
|
|
28
|
+
ok: status.authenticated === true,
|
|
29
|
+
...metadata(status, ['expiresAt', 'remainingSeconds', 'profile']),
|
|
30
|
+
user: metadata(status.user, ['id', 'email', 'name']),
|
|
31
|
+
team: metadata(status.team, ['id', 'name']),
|
|
32
|
+
organization: metadata(status.organization, ['id', 'name']),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
api = {
|
|
37
|
+
ok: false,
|
|
38
|
+
code: 'AUTH_CHECK_FAILED',
|
|
39
|
+
action: `zl --environment ${environment} auth login`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
let database;
|
|
43
|
+
try {
|
|
44
|
+
const result = object(await deps.run(['doctor', '--environment', environment]));
|
|
45
|
+
database =
|
|
46
|
+
result.environment !== environment
|
|
47
|
+
? { ok: false, code: 'ENVIRONMENT_MISMATCH' }
|
|
48
|
+
: {
|
|
49
|
+
ok: result.ok === true,
|
|
50
|
+
installed: true,
|
|
51
|
+
environment,
|
|
52
|
+
awsSso: result.ok === true ? 'ready' : 'unknown',
|
|
53
|
+
...(result.ok === true ? {} : { code: 'CHECK_FAILED' }),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
const code = error instanceof DatabaseToolError
|
|
58
|
+
? error.code
|
|
59
|
+
: object(error).code === 'ENOENT'
|
|
60
|
+
? 'NOT_INSTALLED'
|
|
61
|
+
: 'CHECK_FAILED';
|
|
62
|
+
database = {
|
|
63
|
+
ok: false,
|
|
64
|
+
code,
|
|
65
|
+
installed: code !== 'NOT_INSTALLED',
|
|
66
|
+
awsSso: ['aws_session', 'aws_identity', 'sso_login'].includes(code)
|
|
67
|
+
? 'unavailable'
|
|
68
|
+
: 'unknown',
|
|
69
|
+
action: code === 'NOT_INSTALLED'
|
|
70
|
+
? `Obtain the immutable wheel URL from Platform, then run: ${DATABASE_INSTALL}`
|
|
71
|
+
: `zalos-db login --environment ${environment}; zalos-db doctor --environment ${environment}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
75
|
+
return {
|
|
76
|
+
ok: api.ok && database.ok,
|
|
77
|
+
environment,
|
|
78
|
+
apiUrl: selected.apiUrl,
|
|
79
|
+
nodeVersion: process.version,
|
|
80
|
+
cliVersion: pkg.version,
|
|
81
|
+
api,
|
|
82
|
+
database,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export async function doctorReport(environment) {
|
|
86
|
+
const { authStatus } = await import('./auth.js');
|
|
87
|
+
return buildDoctorReport(environment, { authStatus, run: runDatabaseTool });
|
|
88
|
+
}
|
|
89
|
+
export function registerDoctorCommand(program) {
|
|
90
|
+
program
|
|
91
|
+
.command('doctor')
|
|
92
|
+
.description('Check regional product and database access')
|
|
93
|
+
.action(async () => {
|
|
94
|
+
try {
|
|
95
|
+
const selected = resolveEnvironment({ env: process.env.ZL_ENVIRONMENT });
|
|
96
|
+
const report = await doctorReport(selected.name);
|
|
97
|
+
printJson(report);
|
|
98
|
+
if (!report.ok)
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
handleError(error);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { client } from '../client.js';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { currentEnvironment } from '../environments.js';
|
|
4
|
+
import { downloadToNewFile } from '../utils/download-file.js';
|
|
5
|
+
import { ApiError, ValidationError } from '../utils/errors.js';
|
|
6
|
+
import { filterFields, filterSingle, printJson } from '../utils/output.js';
|
|
7
|
+
import { action, dataRow, fields, integer, isRow, resourceId, } from '../utils/product-api.js';
|
|
8
|
+
export async function inspectRecoveryAction(id, options) {
|
|
9
|
+
const executionId = resourceId(id);
|
|
10
|
+
const bundle = dataRow(await client.get(`/executions/${executionId}/recovery/inspect`));
|
|
11
|
+
if (bundle.schemaVersion !== 1 ||
|
|
12
|
+
bundle.executionId !== executionId ||
|
|
13
|
+
typeof bundle.sha256 !== 'string' ||
|
|
14
|
+
!isRow(bundle.evidence) ||
|
|
15
|
+
!Array.isArray(bundle.evidence.entries) ||
|
|
16
|
+
!bundle.evidence.entries.every(isRow))
|
|
17
|
+
throw new ApiError(502, 'Invalid recovery evidence bundle');
|
|
18
|
+
const encoded = `${JSON.stringify(bundle, null, 2)}\n`;
|
|
19
|
+
if (Buffer.byteLength(encoded) > 4 * 1024 * 1024)
|
|
20
|
+
throw new ApiError(502, 'Recovery evidence bundle exceeds the size limit');
|
|
21
|
+
if (options.output)
|
|
22
|
+
await writeFile(options.output, encoded, { flag: 'wx', mode: 0o600 });
|
|
23
|
+
if (options.json) {
|
|
24
|
+
printJson(bundle);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const timeline = bundle.evidence.entries.map((entry) => {
|
|
28
|
+
const data = isRow(entry.data) ? entry.data : {};
|
|
29
|
+
const outcome = data.outcome ?? data.stage ?? data.status ?? '';
|
|
30
|
+
return `${entry.sequence}. ${entry.at} ${entry.kind}: ${outcome}`;
|
|
31
|
+
});
|
|
32
|
+
// Terminal control characters from remote text must never be interpreted by the terminal.
|
|
33
|
+
const text = [
|
|
34
|
+
`Recovery evidence for ${executionId}`,
|
|
35
|
+
...timeline,
|
|
36
|
+
`Bundle SHA-256: ${bundle.sha256}`,
|
|
37
|
+
...(options.output ? [`Saved to ${options.output}`] : []),
|
|
38
|
+
]
|
|
39
|
+
.join('\n')
|
|
40
|
+
// oxlint-disable-next-line no-control-regex -- intentional terminal output sanitization
|
|
41
|
+
.replace(/[\u0000-\u0008\u000b-\u001f\u007f\u202a-\u202e\u2066-\u2069]/g, '');
|
|
42
|
+
process.stdout.write(`${text}\n`);
|
|
43
|
+
}
|
|
44
|
+
export async function downloadExecutionArtifactAction(artifactId, options) {
|
|
45
|
+
const id = resourceId(artifactId);
|
|
46
|
+
const download = dataRow(await client.get(`/execution-artifacts/${id}/download`));
|
|
47
|
+
if (typeof download.url !== 'string') {
|
|
48
|
+
throw new ValidationError('download', 'The execution artifact API response did not include a download URL');
|
|
49
|
+
}
|
|
50
|
+
const downloaded = await downloadToNewFile(download.url, options.output, {
|
|
51
|
+
allowDataUrl: options.allowDataUrl,
|
|
52
|
+
});
|
|
53
|
+
printJson({ artifactId: id, ...downloaded });
|
|
54
|
+
}
|
|
55
|
+
export async function listExecutionsAction(options) {
|
|
56
|
+
const filter = {};
|
|
57
|
+
if (options.workflowId)
|
|
58
|
+
filter.workflowId = resourceId(options.workflowId);
|
|
59
|
+
if (options.status)
|
|
60
|
+
filter.status = options.status.split(',');
|
|
61
|
+
const query = {
|
|
62
|
+
limit: String(integer(options.limit, 20, 1, 100)),
|
|
63
|
+
offset: String(integer(options.offset, 0)),
|
|
64
|
+
...(Object.keys(filter).length ? { filter: JSON.stringify(filter) } : {}),
|
|
65
|
+
};
|
|
66
|
+
const response = await client.get('/executions', query);
|
|
67
|
+
if (!isRow(response) ||
|
|
68
|
+
!Array.isArray(response.results) ||
|
|
69
|
+
!response.results.every(isRow) ||
|
|
70
|
+
typeof response.count !== 'number')
|
|
71
|
+
throw new ApiError(502, 'Expected execution results and count');
|
|
72
|
+
printJson({
|
|
73
|
+
data: filterFields(response.results, options.fields),
|
|
74
|
+
count: response.count,
|
|
75
|
+
...(Number(query.offset) + response.results.length < response.count
|
|
76
|
+
? { nextOffset: Number(query.offset) + response.results.length }
|
|
77
|
+
: {}),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
export async function viewExecutionAction(id, options, summary = false) {
|
|
81
|
+
printJson(filterSingle(dataRow(await client.get(`/executions/${resourceId(id)}${summary ? '/summary' : ''}`)), options.fields));
|
|
82
|
+
}
|
|
83
|
+
export function registerExecutionCommand(program) {
|
|
84
|
+
const cmd = program.command('execution').description('Inspect workflow executions');
|
|
85
|
+
cmd
|
|
86
|
+
.command('recovery')
|
|
87
|
+
.description('Owner/internal recovery evidence')
|
|
88
|
+
.command('inspect <execution-id>')
|
|
89
|
+
.description('Read the durable recovery timeline without contacting the provider or sandbox')
|
|
90
|
+
.option('--output <path>', 'Save the evidence bundle to a new private JSON file')
|
|
91
|
+
.option('--json', 'Print the full evidence bundle as JSON')
|
|
92
|
+
.action(async (id, options) => action(() => inspectRecoveryAction(id, options)));
|
|
93
|
+
cmd
|
|
94
|
+
.command('list')
|
|
95
|
+
.option('--workflow-id <id>', 'Workflow ID')
|
|
96
|
+
.option('--status <statuses>', 'Comma-separated statuses')
|
|
97
|
+
.option('--limit <n>', 'Maximum results', '20')
|
|
98
|
+
.option('--offset <n>', 'Result offset', '0')
|
|
99
|
+
.action(async (o) => action(() => listExecutionsAction({ ...o, ...fields(program) })));
|
|
100
|
+
for (const verb of ['view', 'summary'])
|
|
101
|
+
cmd
|
|
102
|
+
.command(`${verb} <id>`)
|
|
103
|
+
.action(async (id) => action(() => viewExecutionAction(id, fields(program), verb === 'summary')));
|
|
104
|
+
const artifact = cmd.command('artifact').description('Access immutable execution artifacts');
|
|
105
|
+
artifact
|
|
106
|
+
.command('download <artifact-id>')
|
|
107
|
+
.description('Download an immutable execution artifact through the authenticated API')
|
|
108
|
+
.requiredOption('--output <path>', 'Write content to a new file')
|
|
109
|
+
.action(async (artifactId, options) => action(() => {
|
|
110
|
+
const hostname = new URL(currentEnvironment().apiUrl).hostname;
|
|
111
|
+
return downloadExecutionArtifactAction(artifactId, {
|
|
112
|
+
...options,
|
|
113
|
+
allowDataUrl: ['localhost', '127.0.0.1', '[::1]'].includes(hostname),
|
|
114
|
+
});
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=execution.js.map
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { client } from '../client.js';
|
|
3
|
+
import { currentEnvironment } from '../environments.js';
|
|
4
|
+
import { ValidationError } from '../utils/errors.js';
|
|
5
|
+
import { downloadToNewFile } from '../utils/download-file.js';
|
|
6
|
+
import { filterSingle, printJson } from '../utils/output.js';
|
|
7
|
+
import { action, dataRow, dataRows, fields, isRow, printPage, resourceId, } from '../utils/product-api.js';
|
|
8
|
+
const FILE_FIELDS = 'id,name,fileName,mimeType,size,fileSize,folderId,createdAt,updatedAt';
|
|
9
|
+
async function files(options) {
|
|
10
|
+
return dataRows(await client.get('/team-files', {
|
|
11
|
+
folderId: options.folderId ? resourceId(options.folderId) : 'root',
|
|
12
|
+
})).map((row) => filterSingle(row, FILE_FIELDS));
|
|
13
|
+
}
|
|
14
|
+
export async function listFilesAction(options) {
|
|
15
|
+
printPage(await files(options), options);
|
|
16
|
+
}
|
|
17
|
+
export async function treeFilesAction(options) {
|
|
18
|
+
printPage(dataRows(await client.get('/developer/team-files/tree', { teamId: resourceId(options.teamId) })), options);
|
|
19
|
+
}
|
|
20
|
+
export async function viewFileAction(id, options) {
|
|
21
|
+
const response = options.path
|
|
22
|
+
? await client.get('/team-files/by-path', { path: id })
|
|
23
|
+
: await client.get(`/team-files/${resourceId(id)}`);
|
|
24
|
+
printJson(filterSingle(filterSingle(dataRow(response), FILE_FIELDS), options.fields));
|
|
25
|
+
}
|
|
26
|
+
export async function findFilesAction(query, options) {
|
|
27
|
+
if (!query.trim())
|
|
28
|
+
throw new ValidationError('query', 'query must not be empty');
|
|
29
|
+
if (options.teamId) {
|
|
30
|
+
printPage(dataRows(await client.get('/developer/team-files/search', {
|
|
31
|
+
teamId: resourceId(options.teamId),
|
|
32
|
+
query,
|
|
33
|
+
})), options);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
printPage((await files(options)).filter((row) => String(row.name ?? row.fileName ?? '')
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.includes(query.toLowerCase())), options);
|
|
39
|
+
}
|
|
40
|
+
export async function inspectFileAction(id, options) {
|
|
41
|
+
printJson(filterSingle(dataRow(await client.get(`/developer/team-files/${resourceId(id)}`, {
|
|
42
|
+
teamId: resourceId(options.teamId),
|
|
43
|
+
})), options.fields));
|
|
44
|
+
}
|
|
45
|
+
export async function downloadFileAction(idOrPath, options) {
|
|
46
|
+
if (options.path && options.teamId) {
|
|
47
|
+
throw new ValidationError('path', '--path cannot be combined with --team-id');
|
|
48
|
+
}
|
|
49
|
+
let fileId;
|
|
50
|
+
if (options.path) {
|
|
51
|
+
const file = dataRow(await client.get('/team-files/by-path', { path: idOrPath }));
|
|
52
|
+
if (typeof file.id !== 'string') {
|
|
53
|
+
throw new ValidationError('file', 'The file API response did not include a file ID');
|
|
54
|
+
}
|
|
55
|
+
fileId = resourceId(file.id);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
fileId = resourceId(idOrPath);
|
|
59
|
+
}
|
|
60
|
+
const response = options.teamId
|
|
61
|
+
? await client.get(`/developer/team-files/${fileId}/download`, {
|
|
62
|
+
teamId: resourceId(options.teamId),
|
|
63
|
+
})
|
|
64
|
+
: await client.get(`/team-files/${fileId}/download`);
|
|
65
|
+
const download = dataRow(response);
|
|
66
|
+
if (typeof download.url !== 'string') {
|
|
67
|
+
throw new ValidationError('download', 'The file API response did not include a download URL');
|
|
68
|
+
}
|
|
69
|
+
printJson(await downloadToNewFile(download.url, options.output, {
|
|
70
|
+
allowDataUrl: options.allowDataUrl,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
export async function snapshotFilesAction(options) {
|
|
74
|
+
if (options.teamId) {
|
|
75
|
+
const snapshot = {
|
|
76
|
+
...dataRow(await client.get('/developer/team-files/snapshot', {
|
|
77
|
+
teamId: resourceId(options.teamId),
|
|
78
|
+
})),
|
|
79
|
+
...(options.environment ? { environment: options.environment } : {}),
|
|
80
|
+
command: 'zl file snapshot',
|
|
81
|
+
};
|
|
82
|
+
if (options.output) {
|
|
83
|
+
await writeFile(options.output, `${JSON.stringify(snapshot, null, 2)}\n`, {
|
|
84
|
+
flag: 'wx',
|
|
85
|
+
mode: 0o600,
|
|
86
|
+
});
|
|
87
|
+
printJson({
|
|
88
|
+
output: options.output,
|
|
89
|
+
items: Array.isArray(snapshot.items) ? snapshot.items.length : 0,
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
printJson(snapshot);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (options.output)
|
|
97
|
+
throw new ValidationError('output', '--output requires an explicit --team-id');
|
|
98
|
+
const rows = await files(options);
|
|
99
|
+
printJson({
|
|
100
|
+
folderId: options.folderId ?? null,
|
|
101
|
+
data: rows.map((row) => filterSingle(row, options.fields)),
|
|
102
|
+
count: rows.length,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
export async function seedFilesAction(options) {
|
|
106
|
+
if (options.apply && !options.dryRun && !options.expectedChecksum) {
|
|
107
|
+
throw new ValidationError('expectedChecksum', 'Preview the seed plan, then pass its checksum with --expected-checksum and --apply');
|
|
108
|
+
}
|
|
109
|
+
const sourceScope = options.sourceScope ?? 'files';
|
|
110
|
+
if (sourceScope !== 'files' && sourceScope !== 'generated-outputs') {
|
|
111
|
+
throw new ValidationError('sourceScope', 'sourceScope must be files or generated-outputs');
|
|
112
|
+
}
|
|
113
|
+
const payload = {
|
|
114
|
+
sourceTeamId: resourceId(options.sourceTeamId),
|
|
115
|
+
sourceScope,
|
|
116
|
+
sourcePath: options.sourcePath ?? '/',
|
|
117
|
+
targetPath: options.targetPath ?? '/',
|
|
118
|
+
};
|
|
119
|
+
const plan = dataRow(await client.post('/developer/team-files/seed-plan', payload));
|
|
120
|
+
if (!['ready', 'blocked'].includes(String(plan.status)) || !Array.isArray(plan.operations)) {
|
|
121
|
+
throw new ValidationError('plan', 'Invalid Team Files seed plan response');
|
|
122
|
+
}
|
|
123
|
+
if (!options.apply || options.dryRun) {
|
|
124
|
+
printJson(plan);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (plan.status !== 'ready') {
|
|
128
|
+
throw new ValidationError('plan', 'Cannot apply a blocked Team Files seed plan');
|
|
129
|
+
}
|
|
130
|
+
if (plan.checksum !== options.expectedChecksum) {
|
|
131
|
+
throw new ValidationError('expectedChecksum', 'Team Files changed after preview; review the current plan and checksum');
|
|
132
|
+
}
|
|
133
|
+
printJson({
|
|
134
|
+
applied: true,
|
|
135
|
+
plan,
|
|
136
|
+
data: dataRow(await client.post('/developer/team-files/seed', {
|
|
137
|
+
...payload,
|
|
138
|
+
expectedChecksum: options.expectedChecksum,
|
|
139
|
+
})),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
export async function moveFileAction(id, options) {
|
|
143
|
+
const path = `/team-files/${resourceId(id)}/move`;
|
|
144
|
+
if (options.to !== undefined) {
|
|
145
|
+
if (options.folderId || options.root || !options.to.trim())
|
|
146
|
+
throw new ValidationError('to', '--to must be a nonempty path and cannot be combined with --folder-id or --root');
|
|
147
|
+
const plan = dataRow(await client.post(`/team-files/${id}/move-plan`, { to: options.to }));
|
|
148
|
+
if (!['ready', 'blocked'].includes(String(plan.status)) || !Array.isArray(plan.operations))
|
|
149
|
+
throw new ValidationError('plan', 'Invalid move plan response');
|
|
150
|
+
if (!options.apply || options.dryRun) {
|
|
151
|
+
printJson(plan);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (plan.status !== 'ready' || plan.operations.length !== 1)
|
|
155
|
+
throw new ValidationError('plan', 'Cannot apply blocked or invalid move plan');
|
|
156
|
+
const operation = plan.operations[0];
|
|
157
|
+
if (!isRow(operation) ||
|
|
158
|
+
operation.fileId !== id ||
|
|
159
|
+
operation.status !== 'ready' ||
|
|
160
|
+
(operation.targetFolderId !== null && typeof operation.targetFolderId !== 'string'))
|
|
161
|
+
throw new ValidationError('plan', 'Move plan does not match the requested file');
|
|
162
|
+
const folderId = operation.targetFolderId === null ? null : resourceId(operation.targetFolderId);
|
|
163
|
+
printJson({
|
|
164
|
+
applied: true,
|
|
165
|
+
plan,
|
|
166
|
+
data: filterSingle(dataRow(await client.patch(path, { folderId })), FILE_FIELDS),
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (Boolean(options.folderId) === Boolean(options.root))
|
|
171
|
+
throw new ValidationError('folderId', 'Choose exactly one of --folder-id or --root');
|
|
172
|
+
const payload = {
|
|
173
|
+
folderId: options.folderId ? resourceId(options.folderId) : null,
|
|
174
|
+
};
|
|
175
|
+
if (!options.apply || options.dryRun) {
|
|
176
|
+
printJson({ dryRun: true, method: 'PATCH', path, payload });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
printJson(filterSingle(dataRow(await client.patch(path, payload)), FILE_FIELDS));
|
|
180
|
+
}
|
|
181
|
+
export function registerFileCommand(program) {
|
|
182
|
+
const cmd = program.command('file').description('Inspect, download, and move Team Files');
|
|
183
|
+
cmd
|
|
184
|
+
.command('list')
|
|
185
|
+
.option('--folder-id <id>', 'Folder ID (defaults to root)')
|
|
186
|
+
.option('--limit <n>', 'Maximum results', '100')
|
|
187
|
+
.option('--offset <n>', 'Result offset', '0')
|
|
188
|
+
.action(async (o) => action(() => listFilesAction({ ...o, ...fields(program) })));
|
|
189
|
+
cmd
|
|
190
|
+
.command('tree')
|
|
191
|
+
.description('List enriched Team Files metadata recursively for an explicit team')
|
|
192
|
+
.requiredOption('--team-id <ref>', 'Team public ID (preferred) or UUID; internal human only')
|
|
193
|
+
.option('--limit <n>', 'Maximum results', '100')
|
|
194
|
+
.option('--offset <n>', 'Result offset', '0')
|
|
195
|
+
.action(async (o) => action(() => treeFilesAction({ ...o, ...fields(program) })));
|
|
196
|
+
cmd
|
|
197
|
+
.command('view <id-or-path>')
|
|
198
|
+
.option('--path', 'Resolve an exact Team Files path')
|
|
199
|
+
.action(async (id, o) => action(() => viewFileAction(id, { ...o, ...fields(program) })));
|
|
200
|
+
cmd
|
|
201
|
+
.command('find <query>')
|
|
202
|
+
.description('Search file names within a folder')
|
|
203
|
+
.option('--folder-id <id>', 'Folder ID (defaults to root)')
|
|
204
|
+
.option('--team-id <ref>', 'Search an explicit team by public ID or UUID (internal human only)')
|
|
205
|
+
.option('--limit <n>', 'Maximum results', '100')
|
|
206
|
+
.option('--offset <n>', 'Result offset', '0')
|
|
207
|
+
.action(async (q, o) => action(() => findFilesAction(q, { ...o, ...fields(program) })));
|
|
208
|
+
cmd
|
|
209
|
+
.command('inspect <id>')
|
|
210
|
+
.description('Inspect enriched metadata for a Team File in an explicit team')
|
|
211
|
+
.requiredOption('--team-id <ref>', 'Team public ID (preferred) or UUID; internal human only')
|
|
212
|
+
.action(async (id, o) => action(() => inspectFileAction(id, { ...o, ...fields(program) })));
|
|
213
|
+
cmd
|
|
214
|
+
.command('download <id-or-path>')
|
|
215
|
+
.description('Download a Team File through the authenticated API')
|
|
216
|
+
.requiredOption('--output <path>', 'Write content to a new file')
|
|
217
|
+
.option('--path', 'Resolve an exact Team Files path in the selected team')
|
|
218
|
+
.option('--team-id <ref>', 'Download from a team by public ID or UUID (internal human only)')
|
|
219
|
+
.action(async (id, o) => action(() => {
|
|
220
|
+
const hostname = new URL(currentEnvironment().apiUrl).hostname;
|
|
221
|
+
return downloadFileAction(id, {
|
|
222
|
+
...o,
|
|
223
|
+
allowDataUrl: ['localhost', '127.0.0.1', '[::1]'].includes(hostname),
|
|
224
|
+
});
|
|
225
|
+
}));
|
|
226
|
+
cmd
|
|
227
|
+
.command('snapshot')
|
|
228
|
+
.description('Snapshot metadata within a folder')
|
|
229
|
+
.option('--folder-id <id>', 'Folder ID (defaults to root)')
|
|
230
|
+
.option('--team-id <ref>', 'Snapshot a team by public ID or UUID (internal human only)')
|
|
231
|
+
.option('--output <path>', 'Write JSON to a new file')
|
|
232
|
+
.action(async (o) => action(() => snapshotFilesAction({
|
|
233
|
+
...o,
|
|
234
|
+
...fields(program),
|
|
235
|
+
environment: currentEnvironment().name,
|
|
236
|
+
})));
|
|
237
|
+
cmd
|
|
238
|
+
.command('seed')
|
|
239
|
+
.description('Copy a Team Files subtree from another team into the selected team')
|
|
240
|
+
.requiredOption('--source-team-id <ref>', 'Readable source team public ID (preferred) or UUID; internal human only')
|
|
241
|
+
.option('--source-scope <scope>', 'Source scope: files or generated-outputs', 'files')
|
|
242
|
+
.option('--source-path <path>', 'Source file or folder path', '/')
|
|
243
|
+
.option('--target-path <path>', 'Destination folder path in the selected team', '/')
|
|
244
|
+
.option('--apply', 'Apply the copy plan')
|
|
245
|
+
.option('--expected-checksum <sha256>', 'Checksum printed by the reviewed seed plan')
|
|
246
|
+
.option('--dry-run', 'Preview the copy plan')
|
|
247
|
+
.action(async (o) => action(() => seedFilesAction(o)));
|
|
248
|
+
cmd
|
|
249
|
+
.command('move <id>')
|
|
250
|
+
.option('--to <path>', 'Plan a move to a destination folder path')
|
|
251
|
+
.option('--folder-id <id>', 'Destination folder')
|
|
252
|
+
.option('--root', 'Move to root folder')
|
|
253
|
+
.option('--apply', 'Apply move')
|
|
254
|
+
.option('--dry-run', 'Preview move')
|
|
255
|
+
.action(async (id, o) => action(() => moveFileAction(id, o)));
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=file.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 listLabelsAction(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}/labels`, Object.keys(query).length > 0 ? query : undefined);
|
|
11
|
+
printList(response.data, options.fields, response.nextCursor);
|
|
12
|
+
}
|
|
13
|
+
export function registerLabelCommand(program) {
|
|
14
|
+
const label = program.command('label').description('Manage labels');
|
|
15
|
+
label
|
|
16
|
+
.command('list')
|
|
17
|
+
.description('List labels 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 listLabelsAction({ ...options, fields: globalOpts.fields });
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
handleError(error);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=label.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { client } from '../client.js';
|
|
2
|
+
import { handleError } from '../utils/errors.js';
|
|
3
|
+
import { printList } from '../utils/output.js';
|
|
4
|
+
export async function listMembersAction(options) {
|
|
5
|
+
const response = await client.get('/team/members');
|
|
6
|
+
printList(response.data, options.fields, undefined);
|
|
7
|
+
}
|
|
8
|
+
export function registerMemberCommand(program) {
|
|
9
|
+
const member = program.command('member').description('Manage team members');
|
|
10
|
+
member
|
|
11
|
+
.command('list')
|
|
12
|
+
.description('List members of the current team')
|
|
13
|
+
.action(async () => {
|
|
14
|
+
try {
|
|
15
|
+
const globalOpts = program.optsWithGlobals();
|
|
16
|
+
await listMembersAction(globalOpts);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
handleError(error);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=member.js.map
|