@knowttl/gitea-axi 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +350 -32
- package/axi-credential.schema.json +8 -1
- package/dist/action-values.js +37 -0
- package/dist/api.js +90 -19
- package/dist/attachments.js +171 -0
- package/dist/catalogue.js +65 -0
- package/dist/ci.js +24 -0
- package/dist/credentials.js +78 -6
- package/dist/git.js +89 -0
- package/dist/local-workflows.js +222 -0
- package/dist/main.js +245 -25
- package/dist/private-file.js +3 -1
- package/dist/pulls.js +299 -0
- package/dist/reads.js +15 -9
- package/dist/registry.js +139 -5
- package/dist/releases.js +145 -0
- package/dist/repo-workflows.js +147 -0
- package/dist/search.js +111 -0
- package/dist/stacks.js +486 -0
- package/dist/transport.js +62 -7
- package/dist/writes.js +568 -0
- package/generated/SKILL.md +141 -0
- package/generated/capabilities.json +964 -0
- package/generated/help.txt +5376 -0
- package/generated/host-inventory.json +7000 -151
- package/generated/routes.json +3501 -5
- package/package.json +24 -10
- package/schemas/catalogue.json +2672 -0
package/dist/main.js
CHANGED
|
@@ -4,34 +4,83 @@ import { encode } from '@toon-format/toon';
|
|
|
4
4
|
import { commands } from './registry.js';
|
|
5
5
|
import { CliError } from './error.js';
|
|
6
6
|
import { readRepository } from './repository.js';
|
|
7
|
-
import { configPath, credentials, profileList, redact } from './credentials.js';
|
|
7
|
+
import { configPath, credentials, profileList, redact, registerSecret } from './credentials.js';
|
|
8
8
|
import { Client } from './transport.js';
|
|
9
9
|
import { collect, object, rawPath, rawRead, read } from './api.js';
|
|
10
10
|
import { boundedList, issueFields, listIssues, listOptions, positive, prFields, repository, selectedFields, view, watchChecks } from './reads.js';
|
|
11
|
+
import { tasks, validateTasks } from './ci.js';
|
|
12
|
+
import { search, validateSearch } from './search.js';
|
|
13
|
+
import { issueLabelPlan, rawWritePlan, reconcile, write } from './writes.js';
|
|
14
|
+
import { readPrivateFile } from './private-file.js';
|
|
15
|
+
import { pullPlan, validatePull } from './pulls.js';
|
|
16
|
+
import { repositoryPlan, validateRepositoryWorkflow } from './repo-workflows.js';
|
|
17
|
+
import { releaseListFields, releasePlan, releaseRead, validateRelease } from './releases.js';
|
|
18
|
+
import { actionValues, valuePlan } from './action-values.js';
|
|
19
|
+
import { assetPath, attachToPlan, download, uploadPlan } from './attachments.js';
|
|
20
|
+
import { isLocal, localWorkflow, validateLocal } from './local-workflows.js';
|
|
21
|
+
import { capabilityOutput, commandGap } from './catalogue.js';
|
|
22
|
+
async function input(path) {
|
|
23
|
+
if (path !== '-')
|
|
24
|
+
return readPrivateFile(path, true, 1_000_000);
|
|
25
|
+
process.stdin.setEncoding('utf8');
|
|
26
|
+
let body = '';
|
|
27
|
+
for await (const chunk of process.stdin) {
|
|
28
|
+
body += String(chunk);
|
|
29
|
+
if (Buffer.byteLength(body) > 1_000_000)
|
|
30
|
+
throw new CliError('USAGE', 'Input exceeds 1000000 bytes', 2);
|
|
31
|
+
}
|
|
32
|
+
return body;
|
|
33
|
+
}
|
|
11
34
|
export async function main(args) {
|
|
12
|
-
let operation = 'dashboard';
|
|
35
|
+
let operation = args[0] && !args[0].startsWith('-') ? args[0] : 'dashboard';
|
|
13
36
|
try {
|
|
37
|
+
for (const arg of args) {
|
|
38
|
+
const match = /^(?:--(?:raw-)?field=)?data=([\s\S]*)$/.exec(arg);
|
|
39
|
+
if (match)
|
|
40
|
+
registerSecret(match[1]);
|
|
41
|
+
}
|
|
14
42
|
if (args.some(arg => /^--(?:token|api-key|password|authorization)(?:=|$)/i.test(arg))) {
|
|
15
43
|
throw new CliError('CONFIG_INVALID', 'token and other secret arguments are not accepted');
|
|
16
44
|
}
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
if (!command)
|
|
20
|
-
|
|
45
|
+
const command = args.length === 0 || args[0]?.startsWith('--') ? commands.find(command => command.path[0] === 'dashboard') :
|
|
46
|
+
[...commands].sort((a, b) => b.path.length - a.path.length).find(command => command.path.every((part, index) => args[index] === part));
|
|
47
|
+
if (!command) {
|
|
48
|
+
const children = commands.filter(command => command.path[0] === args[0]);
|
|
49
|
+
if (children.length && args.length === 2 && args[1] === '--help') {
|
|
50
|
+
console.log(encode({ command: 'gitea-axi ' + operation, commands: children.map(command => 'gitea-axi ' + command.path.join(' ')) }));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
throw new CliError('USAGE', children.length ? 'Choose a command: ' + children.map(command => 'gitea-axi ' + command.path.join(' ')).join(', ') : 'Unknown command; use gitea-axi --help', 2);
|
|
54
|
+
}
|
|
55
|
+
const path = command.path;
|
|
21
56
|
operation = command.path.join(' ');
|
|
22
57
|
const rest = args[0]?.startsWith('--') || !args.length ? args : args.slice(path.length);
|
|
23
58
|
let parsed;
|
|
24
59
|
try {
|
|
60
|
+
const options = Object.fromEntries(Object.entries(command.flags).map(([name, flag]) => [name, { type: flag.type,
|
|
61
|
+
...(flag.multiple ? { multiple: true } : {}), ...(flag.short ? { short: flag.short } : {}) }]));
|
|
62
|
+
const tokens = parseArgs({ args: rest, allowPositionals: true, strict: false, tokens: true, options }).tokens;
|
|
63
|
+
const normalized = tokens.map(token => {
|
|
64
|
+
if (token.kind === 'positional')
|
|
65
|
+
return token.value;
|
|
66
|
+
if (token.kind === 'option-terminator')
|
|
67
|
+
return '--';
|
|
68
|
+
const boolean = command.flags[token.name]?.type === 'boolean' && /^(true|false)$/.test(token.value ?? '');
|
|
69
|
+
return '--' + token.name + (token.value !== undefined && !boolean ? '=' + token.value : '');
|
|
70
|
+
});
|
|
25
71
|
parsed = parseArgs({
|
|
26
|
-
args:
|
|
27
|
-
options: Object.fromEntries(Object.entries(command.flags).map(([name, flag]) => [name, { type: flag.type }])),
|
|
72
|
+
args: normalized, allowPositionals: true, strict: true, tokens: true, options,
|
|
28
73
|
});
|
|
74
|
+
for (const token of tokens) {
|
|
75
|
+
if (token.kind === 'option' && command.flags[token.name]?.type === 'boolean' && /^(true|false)$/.test(token.value ?? ''))
|
|
76
|
+
parsed.values[token.name] = token.value === 'true';
|
|
77
|
+
}
|
|
29
78
|
}
|
|
30
79
|
catch {
|
|
31
80
|
throw new CliError('USAGE', 'Invalid arguments; valid flags: ' + Object.keys(command.flags).map(f => '--' + f).join(', '), 2);
|
|
32
81
|
}
|
|
33
82
|
const { values, positionals } = parsed;
|
|
34
|
-
const names = parsed.tokens.
|
|
83
|
+
const names = parsed.tokens.flatMap(token => token.kind === 'option' && !command.flags[token.name]?.multiple ? [token.name] : []);
|
|
35
84
|
if (new Set(names).size !== names.length) {
|
|
36
85
|
throw new CliError('USAGE', 'Choose each flag only once; --repo requires exactly one selector', 2);
|
|
37
86
|
}
|
|
@@ -44,15 +93,106 @@ export async function main(args) {
|
|
|
44
93
|
return;
|
|
45
94
|
}
|
|
46
95
|
const options = values;
|
|
47
|
-
|
|
96
|
+
if (operation === 'capabilities') {
|
|
97
|
+
if (positionals.length > 1)
|
|
98
|
+
throw new CliError('USAGE', 'Use capabilities [C001-C124]', 2);
|
|
99
|
+
console.log(encode(capabilityOutput(commands, prFields, positionals[0])));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (command.gap)
|
|
103
|
+
throw new CliError('CAPABILITY_GAP', command.gap);
|
|
104
|
+
for (const name of Object.keys(values)) {
|
|
105
|
+
if (command.flags[name]?.gap)
|
|
106
|
+
throw new CliError('CAPABILITY_GAP', command.flags[name].gap);
|
|
107
|
+
}
|
|
108
|
+
if (operation === 'api' && (options.method === 'HEAD' || positionals[0] === 'HEAD'))
|
|
109
|
+
throw new CliError('CAPABILITY_GAP', commandGap('api HEAD'));
|
|
110
|
+
if ((process.env.GITEA_AXI_PROFILE === 'artium' || process.env.GITEA_AXI_CREDENTIAL_REVISION !== undefined) &&
|
|
111
|
+
['origin', 'profile', 'config', 'hostname', 'allow-insecure-http'].some(name => options[name] !== undefined)) {
|
|
112
|
+
throw new CliError('READ_ONLY', 'Hosted calls refuse credential and configuration overrides');
|
|
113
|
+
}
|
|
114
|
+
if (isLocal(operation, options)) {
|
|
115
|
+
validateLocal(operation, options);
|
|
116
|
+
const identity = await credentials(typeof options.origin === 'string' ? options.origin : undefined, Boolean(options['allow-insecure-http']), {
|
|
117
|
+
...(typeof options.config === 'string' ? { config: options.config } : {}), ...(typeof options.profile === 'string' ? { profile: options.profile } : {}),
|
|
118
|
+
...(typeof options.hostname === 'string' ? { hostname: options.hostname } : {}),
|
|
119
|
+
});
|
|
120
|
+
const output = await localWorkflow(new Client(identity.origin, identity.token, 300_000), identity, operation, positionals, options);
|
|
121
|
+
const rendered = encode(redact(output));
|
|
122
|
+
if (rendered.length > 100_000)
|
|
123
|
+
throw new CliError('OUTPUT_LIMIT', 'Rendered output exceeds 100000 characters');
|
|
124
|
+
console.log(rendered);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const valuesCommand = operation.startsWith('secret ') || operation.startsWith('variable ');
|
|
128
|
+
const secretCommand = operation.startsWith('secret ');
|
|
129
|
+
let protectedValue;
|
|
130
|
+
if (valuesCommand) {
|
|
131
|
+
if (options.org && options.repo)
|
|
132
|
+
throw new CliError('USAGE', 'Choose --org or --repo', 2);
|
|
133
|
+
if (options.org && (!/^[A-Za-z0-9_.-]+$/.test(String(options.org)) || ['.', '..'].includes(String(options.org))))
|
|
134
|
+
throw new CliError('USAGE', 'Select an organization name', 2);
|
|
135
|
+
if (operation.endsWith(' set')) {
|
|
136
|
+
if (options.input !== undefined && options.body !== undefined || options.input === undefined && options.body === undefined)
|
|
137
|
+
throw new CliError('USAGE', 'Choose exactly one value input', 2);
|
|
138
|
+
protectedValue = options.input !== undefined ? await input(String(options.input)) : String(options.body);
|
|
139
|
+
if (secretCommand)
|
|
140
|
+
registerSecret(protectedValue);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const repositoryMutation = operation.startsWith('repo ') && command.effect === 'write';
|
|
144
|
+
const releasing = operation.startsWith('release ');
|
|
145
|
+
if (repositoryMutation)
|
|
146
|
+
validateRepositoryWorkflow(operation, options);
|
|
147
|
+
if (releasing) {
|
|
148
|
+
validateRelease(options);
|
|
149
|
+
if (options.notes !== undefined)
|
|
150
|
+
options.body = options.notes;
|
|
151
|
+
if (options['notes-file'] !== undefined)
|
|
152
|
+
options['body-file'] = options['notes-file'];
|
|
153
|
+
}
|
|
154
|
+
const transferring = /^(?:issue|pr|release) (?:upload|download)$/.test(operation);
|
|
155
|
+
if (operation.startsWith('pr ') && command.effect === 'write' && !transferring)
|
|
156
|
+
validatePull(operation, options);
|
|
157
|
+
if (operation.startsWith('workflow ') || operation.startsWith('run ') && operation !== 'run list') {
|
|
158
|
+
throw new CliError('CAPABILITY_GAP', operation + ' is pending: the 1.23.4 contract has task summaries only; 1.24.0 candidates require commissioning. Mutating controls also await the shared write gate');
|
|
159
|
+
}
|
|
160
|
+
const searching = operation.startsWith('search ');
|
|
161
|
+
if (searching) {
|
|
162
|
+
if (positionals.length !== 1)
|
|
163
|
+
throw new CliError('USAGE', 'Select exactly one quoted search query', 2);
|
|
164
|
+
validateSearch(path[1], positionals[0], options);
|
|
165
|
+
}
|
|
166
|
+
if (options['body-file'] !== undefined) {
|
|
167
|
+
if (options.body !== undefined)
|
|
168
|
+
throw new CliError('USAGE', 'Choose --body or --body-file', 2);
|
|
169
|
+
options.body = await input(String(options['body-file']));
|
|
170
|
+
}
|
|
171
|
+
const takesNumber = ['issue view', 'pr view', 'pr checks', 'pr diff', 'issue download', 'pr download'].includes(operation) ||
|
|
172
|
+
(command.effect === 'write' && (operation.startsWith('issue ') || operation.startsWith('pr ')) && !operation.endsWith(' create'));
|
|
48
173
|
if (takesNumber && (positionals.length !== 1 || !/^[1-9]\d*$/.test(positionals[0])))
|
|
49
174
|
throw new CliError('USAGE', 'Select exactly one positive item number', 2);
|
|
50
|
-
|
|
175
|
+
const takesLabel = operation === 'label edit' || operation === 'label delete';
|
|
176
|
+
const takesValue = valuesCommand && !operation.endsWith(' list');
|
|
177
|
+
if (takesValue && (positionals.length !== 1 || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(positionals[0])))
|
|
178
|
+
throw new CliError('USAGE', 'Select one secret or variable name using letters, digits and underscores', 2);
|
|
179
|
+
if (takesLabel && positionals.length !== 1)
|
|
180
|
+
throw new CliError('USAGE', 'Select one exact label name or ID', 2);
|
|
181
|
+
const takesTag = releasing && operation !== 'release list';
|
|
182
|
+
if (takesTag && (positionals.length !== 1 || !positionals[0]))
|
|
183
|
+
throw new CliError('USAGE', 'Select exactly one release tag', 2);
|
|
184
|
+
if (repositoryMutation && positionals.length > 1)
|
|
185
|
+
throw new CliError('USAGE', 'Select at most one repository', 2);
|
|
186
|
+
if (!takesNumber && !takesLabel && !takesTag && !takesValue && !repositoryMutation && !searching && operation !== 'write reconcile' && operation !== 'api' && operation !== 'org view' && positionals.length)
|
|
51
187
|
throw new CliError('USAGE', 'Unexpected positional argument', 2);
|
|
188
|
+
if (operation === 'write reconcile' && positionals.length !== 1)
|
|
189
|
+
throw new CliError('USAGE', 'Select one confirmation ID', 2);
|
|
52
190
|
if (operation === 'org view' && (positionals.length !== 1 || !/^[A-Za-z0-9_.-]+$/.test(positionals[0])))
|
|
53
191
|
throw new CliError('USAGE', 'Select one organization name', 2);
|
|
54
192
|
if (operation.endsWith('list'))
|
|
55
193
|
listOptions(options);
|
|
194
|
+
if (operation === 'run list')
|
|
195
|
+
validateTasks(options);
|
|
56
196
|
if (operation === 'issue view' || operation === 'issue list')
|
|
57
197
|
selectedFields(options, issueFields);
|
|
58
198
|
if (operation === 'pr view')
|
|
@@ -68,12 +208,55 @@ export async function main(args) {
|
|
|
68
208
|
positive(options.interval, 5, 60);
|
|
69
209
|
}
|
|
70
210
|
let apiPath;
|
|
211
|
+
let method = 'GET';
|
|
212
|
+
let payload;
|
|
71
213
|
if (operation === 'api') {
|
|
72
|
-
if (positionals.length === 2
|
|
73
|
-
throw new CliError('
|
|
214
|
+
if (options.method && positionals.length === 2)
|
|
215
|
+
throw new CliError('USAGE', 'Choose -X or a positional method once', 2);
|
|
216
|
+
method = positionals.length === 2 ? positionals[0] : String(options.method ?? 'GET');
|
|
217
|
+
if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method))
|
|
218
|
+
throw new CliError('USAGE', 'Use GET, POST, PUT, PATCH or DELETE', 2);
|
|
74
219
|
if (positionals.length < 1 || positionals.length > 2)
|
|
75
220
|
throw new CliError('USAGE', 'Use gitea-axi api [GET] <relative-path>', 2);
|
|
76
|
-
apiPath = rawPath(positionals.at(-1));
|
|
221
|
+
apiPath = rawPath(positionals.at(-1), method);
|
|
222
|
+
const fields = parsed.tokens.filter(token => token.kind === 'option' && ['field', 'raw-field'].includes(token.name));
|
|
223
|
+
if (fields.length) {
|
|
224
|
+
if (options.input || method === 'GET')
|
|
225
|
+
throw new CliError('USAGE', 'Request fields require a write method and cannot combine with --input', 2);
|
|
226
|
+
const body = {};
|
|
227
|
+
for (const token of fields) {
|
|
228
|
+
if (token.kind !== 'option')
|
|
229
|
+
continue;
|
|
230
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)=([\s\S]*)$/.exec(token.value ?? '');
|
|
231
|
+
if (!match || ['__proto__', 'prototype', 'constructor'].includes(match[1]) || Object.hasOwn(body, match[1]))
|
|
232
|
+
throw new CliError('USAGE', 'Choose unique key=value request fields', 2);
|
|
233
|
+
if (/secret|token|password|authorization|api.?key/i.test(match[1]) || /\/secrets\//.test(apiPath))
|
|
234
|
+
throw new CliError('CONFIG_INVALID', 'Secret fields require protected --input');
|
|
235
|
+
const value = match[2];
|
|
236
|
+
body[match[1]] = token.name === 'field' && /^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)$/.test(value) ? JSON.parse(value) : value;
|
|
237
|
+
}
|
|
238
|
+
if (Buffer.byteLength(JSON.stringify(body)) > 1_000_000)
|
|
239
|
+
throw new CliError('USAGE', 'Request exceeds 1000000 bytes', 2);
|
|
240
|
+
payload = body;
|
|
241
|
+
}
|
|
242
|
+
if (method !== 'GET' && (!options['allow-write'] || process.env.GITEA_AXI_READ_ONLY && process.env.GITEA_AXI_READ_ONLY !== 'false'))
|
|
243
|
+
throw new CliError('READ_ONLY', 'Writes require an exact operation grant and a writable policy');
|
|
244
|
+
if (options.input) {
|
|
245
|
+
if (method === 'GET')
|
|
246
|
+
throw new CliError('USAGE', '--input requires a write method', 2);
|
|
247
|
+
const body = await input(String(options.input));
|
|
248
|
+
registerSecret(body);
|
|
249
|
+
if (Buffer.byteLength(body) > 1_000_000)
|
|
250
|
+
throw new CliError('USAGE', 'Request exceeds 1000000 bytes', 2);
|
|
251
|
+
try {
|
|
252
|
+
payload = JSON.parse(body);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
throw new CliError('USAGE', 'Request must be valid JSON', 2);
|
|
256
|
+
}
|
|
257
|
+
if (/\/actions\/secrets\//.test(apiPath) && typeof object(payload).data === 'string')
|
|
258
|
+
registerSecret(String(object(payload).data));
|
|
259
|
+
}
|
|
77
260
|
}
|
|
78
261
|
if (options.config && (typeof options.config !== 'string' || !isAbsolute(options.config)))
|
|
79
262
|
throw new CliError('USAGE', '--config must be absolute', 2);
|
|
@@ -87,17 +270,33 @@ export async function main(args) {
|
|
|
87
270
|
}
|
|
88
271
|
const identity = await credentials(typeof values.origin === 'string' ? values.origin : undefined, Boolean(values['allow-insecure-http']), {
|
|
89
272
|
...(typeof options.config === 'string' ? { config: options.config } : {}), ...(typeof options.profile === 'string' ? { profile: options.profile } : {}),
|
|
273
|
+
...(typeof options.hostname === 'string' ? { hostname: options.hostname } : {}),
|
|
90
274
|
});
|
|
91
275
|
const client = new Client(identity.origin, identity.token, operation === 'pr checks' && options.watch ? positive(options.timeout, 30, 3600) * 1000 : 30_000);
|
|
92
276
|
let output;
|
|
93
277
|
if (operation === 'auth status')
|
|
94
278
|
output = { profile: identity.profile ?? 'anonymous', origin: identity.origin, source: identity.source ?? 'anonymous', authenticated: Boolean(identity.token) };
|
|
95
|
-
else if (operation === 'doctor')
|
|
96
|
-
|
|
279
|
+
else if (operation === 'doctor') {
|
|
280
|
+
const account = object(await read(client, '/api/v1/user'));
|
|
281
|
+
output = { origin: identity.origin, identity: { id: account.id, login: account.login } };
|
|
282
|
+
}
|
|
283
|
+
else if (searching)
|
|
284
|
+
output = await search(client, path[1], positionals[0], options);
|
|
285
|
+
else if (operation === 'write reconcile')
|
|
286
|
+
output = await reconcile(client, positionals[0]);
|
|
97
287
|
else if (operation === 'api')
|
|
98
|
-
output = await rawRead(client, apiPath)
|
|
288
|
+
output = method === 'GET' ? await rawRead(client, apiPath) :
|
|
289
|
+
await write(client, identity, await rawWritePlan(client, method, apiPath, payload), options);
|
|
99
290
|
else if (operation === 'org view')
|
|
100
291
|
output = { organization: await read(client, '/api/v1/orgs/' + encodeURIComponent(positionals[0])) };
|
|
292
|
+
else if (valuesCommand) {
|
|
293
|
+
const scope = options.org ? '/api/v1/orgs/' + encodeURIComponent(String(options.org)) : '/api/v1/repos/' + repository(typeof options.repo === 'string' ? options.repo : undefined, identity.origin).split('/').map(encodeURIComponent).join('/');
|
|
294
|
+
const path = scope + '/actions/' + (secretCommand ? 'secrets' : 'variables');
|
|
295
|
+
output = operation.endsWith(' list') ? boundedList({ items: await actionValues(client, path), total: null, next: null, truncated: false }, positive(options.limit, 30, 1000), secretCommand ? ['name'] : ['name', 'data']) :
|
|
296
|
+
await write(client, identity, await valuePlan(client, path, positionals[0], operation.split(' ')[1], protectedValue), options);
|
|
297
|
+
}
|
|
298
|
+
else if (repositoryMutation)
|
|
299
|
+
output = await write(client, identity, await repositoryPlan(client, operation, positionals[0], options), options);
|
|
101
300
|
else if (operation === 'repo list') {
|
|
102
301
|
if (options.fields)
|
|
103
302
|
throw new CliError('USAGE', 'repo list supports its compact identity fields only', 2);
|
|
@@ -105,16 +304,35 @@ export async function main(args) {
|
|
|
105
304
|
output = boundedList(await collect(client, path, options.org ? undefined : 'data'), positive(options.limit, 30, 1000), ['id', 'full_name', 'default_branch']);
|
|
106
305
|
}
|
|
107
306
|
else {
|
|
108
|
-
|
|
307
|
+
if (command.effect === 'write' && (!options['allow-write'] || process.env.GITEA_AXI_READ_ONLY && process.env.GITEA_AXI_READ_ONLY !== 'false'))
|
|
308
|
+
throw new CliError('READ_ONLY', 'Writes require an exact operation grant and a writable policy');
|
|
309
|
+
const repo = repository(typeof values.repo === 'string' ? values.repo : undefined, identity.origin);
|
|
109
310
|
const base = '/api/v1/repos/' + repo.split('/').map(encodeURIComponent).join('/');
|
|
110
|
-
if (
|
|
311
|
+
if (transferring) {
|
|
312
|
+
const path = await assetPath(client, base, operation.split(' ')[0], positionals[0]);
|
|
313
|
+
output = operation.endsWith(' upload') ? await write(client, identity, await uploadPlan(client, path, options), options) : await download(client, path, options);
|
|
314
|
+
}
|
|
315
|
+
else if (releasing)
|
|
316
|
+
output = command.effect === 'write' ? await write(client, identity, await attachToPlan(client, await releasePlan(client, operation, base, positionals[0], options), operation, base, positionals[0], options), options) : await releaseRead(client, operation, base, positionals[0], options);
|
|
317
|
+
else if (command.effect === 'write')
|
|
318
|
+
output = await write(client, identity, await attachToPlan(client, await (operation.startsWith('pr ') ? pullPlan : issueLabelPlan)(client, operation, base, positionals[0], options), operation, base, positionals[0], options), options);
|
|
319
|
+
else if (operation === 'repo view')
|
|
111
320
|
output = await readRepository(identity.origin, repo, identity.token);
|
|
112
321
|
else if (operation === 'dashboard') {
|
|
113
322
|
const sections = {};
|
|
114
|
-
for (const [name, path,
|
|
323
|
+
for (const [name, path, listCommand] of [['repository', base], ['issues', base + '/issues?state=open&type=issues', 'issue'], ['pulls', base + '/pulls?state=open', 'pr'], ['labels', base + '/labels', 'label'], ['releases', base + '/releases', 'release'], ['runs', base + '/actions/tasks', 'run']]) {
|
|
115
324
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
325
|
+
if (name === 'repository') {
|
|
326
|
+
const row = object(await read(client, path));
|
|
327
|
+
sections[name] = { status: 'ok', data: Object.fromEntries(['id', 'full_name', 'private', 'default_branch', 'open_issues_count', 'open_pr_counter', 'updated_at'].map(key => [key, row[key] ?? null])) };
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
const list = name === 'runs' ? await tasks(client, base, { limit: '5' }) : boundedList(await collect(client, path), 5, name === 'labels' ? ['id', 'name', 'color'] : name === 'releases' ? releaseListFields : ['id', 'number', 'title', 'state']);
|
|
331
|
+
sections[name] = { status: 'ok', ...list, ...(list.truncated ? { help: [
|
|
332
|
+
`Run gitea-axi ${listCommand} list --repo ${repo} --limit 1000 to see more items.`,
|
|
333
|
+
...(list.help ?? []).filter(hint => !hint.startsWith('Raise --limit')),
|
|
334
|
+
] } : {}) };
|
|
335
|
+
}
|
|
118
336
|
}
|
|
119
337
|
catch (error) {
|
|
120
338
|
if (!(error instanceof CliError))
|
|
@@ -129,8 +347,10 @@ export async function main(args) {
|
|
|
129
347
|
output = await listIssues(client, base, operation === 'pr list' ? 'pr' : 'issue', options);
|
|
130
348
|
else if (operation === 'issue view' || operation === 'pr view')
|
|
131
349
|
output = await view(client, base, operation === 'pr view' ? 'pr' : 'issue', positionals[0], options);
|
|
132
|
-
else if (
|
|
133
|
-
output =
|
|
350
|
+
else if (operation === 'run list')
|
|
351
|
+
output = await tasks(client, base, options);
|
|
352
|
+
else if (operation === 'label list')
|
|
353
|
+
output = boundedList(await collect(client, base + '/labels'), positive(options.limit, 30, 1000), ['id', 'name', 'color']);
|
|
134
354
|
else if (operation === 'pr checks') {
|
|
135
355
|
const result = await watchChecks(client, base, positionals[0], options);
|
|
136
356
|
process.exitCode = result.exitCode;
|
|
@@ -150,7 +370,7 @@ export async function main(args) {
|
|
|
150
370
|
}
|
|
151
371
|
catch (error) {
|
|
152
372
|
const failure = error instanceof CliError ? error : new CliError('INTERNAL_ERROR', 'Unable to read the repository');
|
|
153
|
-
console.log(encode(redact({ error: { code: failure.code, operation, reason: failure.message }, help: 'gitea-axi ' + operation + ' --help' })));
|
|
373
|
+
console.log(encode(redact({ error: { code: failure.code, operation, reason: failure.message }, help: commands.some(command => command.path.join(' ') === operation || command.path[0] === operation) ? 'gitea-axi ' + operation + ' --help' : 'gitea-axi --help' })));
|
|
154
374
|
process.exitCode = failure.exitCode;
|
|
155
375
|
}
|
|
156
376
|
}
|
package/dist/private-file.js
CHANGED
|
@@ -3,7 +3,7 @@ import { constants } from 'node:fs';
|
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { CliError } from './error.js';
|
|
6
|
-
export async function readPrivateFile(path, secret = false) {
|
|
6
|
+
export async function readPrivateFile(path, secret = false, maxBytes) {
|
|
7
7
|
if (secret && process.platform === 'win32')
|
|
8
8
|
throw new CliError('CONFIG_INVALID', 'File SecretRef protection cannot be verified on Windows; use env SecretRefs');
|
|
9
9
|
const info = await lstat(path).catch(() => { throw new CliError('CONFIG_INSECURE', 'Private file is unavailable'); });
|
|
@@ -14,6 +14,8 @@ export async function readPrivateFile(path, secret = false) {
|
|
|
14
14
|
});
|
|
15
15
|
try {
|
|
16
16
|
const stat = await file.stat();
|
|
17
|
+
if (maxBytes !== undefined && stat.size > maxBytes)
|
|
18
|
+
throw new CliError('USAGE', 'Input exceeds ' + maxBytes + ' bytes', 2);
|
|
17
19
|
if (!stat.isFile() || (process.platform !== 'win32' &&
|
|
18
20
|
(stat.uid !== process.getuid() || (stat.mode & (secret ? 0o077 : 0o022)) !== 0))) {
|
|
19
21
|
throw new CliError('CONFIG_INSECURE', 'Private file must be owned and protected; use chmod 600');
|