@simplysf/simply-gitlab-core 0.1.0

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.
Files changed (59) hide show
  1. package/README.md +76 -0
  2. package/lib/auth.d.ts +9 -0
  3. package/lib/auth.js +26 -0
  4. package/lib/auth.js.map +1 -0
  5. package/lib/cicd.d.ts +29 -0
  6. package/lib/cicd.js +56 -0
  7. package/lib/cicd.js.map +1 -0
  8. package/lib/commits.d.ts +34 -0
  9. package/lib/commits.js +101 -0
  10. package/lib/commits.js.map +1 -0
  11. package/lib/config.d.ts +21 -0
  12. package/lib/config.js +76 -0
  13. package/lib/config.js.map +1 -0
  14. package/lib/env-file.d.ts +11 -0
  15. package/lib/env-file.js +98 -0
  16. package/lib/env-file.js.map +1 -0
  17. package/lib/errors.d.ts +31 -0
  18. package/lib/errors.js +66 -0
  19. package/lib/errors.js.map +1 -0
  20. package/lib/gitlab-client.d.ts +137 -0
  21. package/lib/gitlab-client.js +250 -0
  22. package/lib/gitlab-client.js.map +1 -0
  23. package/lib/http.d.ts +82 -0
  24. package/lib/http.js +285 -0
  25. package/lib/http.js.map +1 -0
  26. package/lib/index.d.ts +17 -0
  27. package/lib/index.js +46 -0
  28. package/lib/index.js.map +1 -0
  29. package/lib/json-input.d.ts +28 -0
  30. package/lib/json-input.js +84 -0
  31. package/lib/json-input.js.map +1 -0
  32. package/lib/merge-requests.d.ts +40 -0
  33. package/lib/merge-requests.js +89 -0
  34. package/lib/merge-requests.js.map +1 -0
  35. package/lib/output.d.ts +14 -0
  36. package/lib/output.js +65 -0
  37. package/lib/output.js.map +1 -0
  38. package/lib/redaction.d.ts +26 -0
  39. package/lib/redaction.js +71 -0
  40. package/lib/redaction.js.map +1 -0
  41. package/lib/repository-files.d.ts +50 -0
  42. package/lib/repository-files.js +70 -0
  43. package/lib/repository-files.js.map +1 -0
  44. package/lib/search.d.ts +20 -0
  45. package/lib/search.js +92 -0
  46. package/lib/search.js.map +1 -0
  47. package/lib/tables.d.ts +21 -0
  48. package/lib/tables.js +105 -0
  49. package/lib/tables.js.map +1 -0
  50. package/lib/testing.d.ts +16 -0
  51. package/lib/testing.js +63 -0
  52. package/lib/testing.js.map +1 -0
  53. package/lib/text.d.ts +10 -0
  54. package/lib/text.js +80 -0
  55. package/lib/text.js.map +1 -0
  56. package/lib/write-safety.d.ts +19 -0
  57. package/lib/write-safety.js +45 -0
  58. package/lib/write-safety.js.map +1 -0
  59. package/package.json +130 -0
package/lib/output.js ADDED
@@ -0,0 +1,65 @@
1
+ /*
2
+ * Copyright (c) 2026, SimplySF.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ const EM_DASH = '—';
17
+ import { stripControl, stripControlOneLine } from './text.js';
18
+ export { stripControl, stripControlOneLine } from './text.js';
19
+ /** Renders any API value as one line of terminal text; missing values read as an em dash. */
20
+ function cell(value) {
21
+ if (value === null || value === undefined || value === '')
22
+ return EM_DASH;
23
+ if (typeof value === 'string')
24
+ return stripControlOneLine(value);
25
+ if (typeof value === 'number' || typeof value === 'boolean')
26
+ return String(value);
27
+ return stripControl(JSON.stringify(value));
28
+ }
29
+ /** Right-pads to `width`, counting characters (adequate for the ASCII-ish fields we render). */
30
+ function pad(text, width) {
31
+ return text.length >= width ? text : text + ' '.repeat(width - text.length);
32
+ }
33
+ /** Aligned `Label: value` lines for a single entity. */
34
+ export function formatKeyValue(pairs) {
35
+ const shown = pairs.filter(([, value]) => value !== undefined);
36
+ if (shown.length === 0)
37
+ return '';
38
+ // Reduced for the same reason as formatTable's widths, so the dangerous pattern is not left
39
+ // sitting one function above the fix for it.
40
+ const labelWidth = shown.reduce((widest, [label]) => Math.max(widest, label.length), 0);
41
+ return shown.map(([label, value]) => `${pad(`${label}:`, labelWidth + 1)} ${cell(value)}`).join('\n');
42
+ }
43
+ /**
44
+ * Two-space-separated columns with a header rule. Every column is sized to its widest cell, and
45
+ * the last column is never padded so trailing whitespace never lands in a pipeline.
46
+ */
47
+ export function formatTable(rows, columns) {
48
+ if (rows.length === 0)
49
+ return '';
50
+ const body = rows.map((row) => columns.map((column) => cell(column.value(row))));
51
+ // Reduced rather than spread into Math.max: `...body.map(...)` puts one argument on the stack
52
+ // per row, which throws RangeError somewhere past 100k rows. An instance can return that many
53
+ // links, and an ungraceful crash is a worse answer than a wide table.
54
+ const widths = columns.map((column, index) => body.reduce((widest, cells) => Math.max(widest, (cells[index] ?? '').length), column.header.length));
55
+ const render = (cells) => cells
56
+ .map((text, index) => (index === cells.length - 1 ? text : pad(text, widths[index] ?? 0)))
57
+ .join(' ')
58
+ .trimEnd();
59
+ return [
60
+ render(columns.map((column) => column.header)),
61
+ render(widths.map((width) => '─'.repeat(width))),
62
+ ...body.map(render),
63
+ ].join('\n');
64
+ }
65
+ //# sourceMappingURL=output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output.js","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { EnvLike } from './config.js';
2
+ /** The environment variables that carry credentials, and so must never reach any output. */
3
+ export declare const SECRET_ENV: readonly ["GITLAB_TOKEN"];
4
+ /**
5
+ * Keeps the values worth redacting out of whatever a caller collected — the environment, a
6
+ * command line, a config file — and drops the ones too short to be a credential.
7
+ */
8
+ export declare function collectSecrets(values: Iterable<string | undefined>): Set<string>;
9
+ /** The credential values present in an environment. */
10
+ export declare function secretValues(env?: EnvLike): Set<string>;
11
+ /** Blanks out any credential that could otherwise ride along in a message. */
12
+ export declare function redactSecrets(message: string, secrets: Iterable<string>): string;
13
+ /**
14
+ * Applies the same guards to a response body that an error message gets.
15
+ *
16
+ * A message that was redacted and stripped while the `body` beside it — the same bytes from the
17
+ * same response — went out untouched is a credential disclosure whenever the far side echoes
18
+ * the token back: a captive portal, a proxy, or an agency gateway does exactly that.
19
+ * `JSON.stringify` escapes C0 but not C1, U+2028/U+2029, or the invisible ranges, so a hostile
20
+ * body would also reach an agent's context with live escape sequences intact.
21
+ *
22
+ * Recursive because the body is arbitrary server JSON — GitLab nests its messages under
23
+ * `message` and `error` in shapes that vary by endpoint — so guarding only a top-level string
24
+ * would miss the common case.
25
+ */
26
+ export declare function sanitiseDeep(value: unknown, secrets: Iterable<string>, depth?: number): unknown;
@@ -0,0 +1,71 @@
1
+ /*
2
+ * Copyright (c) 2026, SimplySF.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import process from 'node:process';
17
+ import { stripControl } from './text.js';
18
+ /** The environment variables that carry credentials, and so must never reach any output. */
19
+ export const SECRET_ENV = ['GITLAB_TOKEN'];
20
+ /** Shorter values would mangle unrelated text; real tokens are far longer than this. */
21
+ const MIN_SECRET_LENGTH = 8;
22
+ /**
23
+ * Keeps the values worth redacting out of whatever a caller collected — the environment, a
24
+ * command line, a config file — and drops the ones too short to be a credential.
25
+ */
26
+ export function collectSecrets(values) {
27
+ const secrets = new Set();
28
+ for (const value of values) {
29
+ if (value !== undefined && value.length >= MIN_SECRET_LENGTH)
30
+ secrets.add(value);
31
+ }
32
+ return secrets;
33
+ }
34
+ /** The credential values present in an environment. */
35
+ export function secretValues(env = process.env) {
36
+ return collectSecrets(SECRET_ENV.map((name) => env[name]));
37
+ }
38
+ /** Blanks out any credential that could otherwise ride along in a message. */
39
+ export function redactSecrets(message, secrets) {
40
+ let redacted = message;
41
+ for (const secret of secrets)
42
+ redacted = redacted.replaceAll(secret, '<redacted>');
43
+ return redacted;
44
+ }
45
+ /**
46
+ * Applies the same guards to a response body that an error message gets.
47
+ *
48
+ * A message that was redacted and stripped while the `body` beside it — the same bytes from the
49
+ * same response — went out untouched is a credential disclosure whenever the far side echoes
50
+ * the token back: a captive portal, a proxy, or an agency gateway does exactly that.
51
+ * `JSON.stringify` escapes C0 but not C1, U+2028/U+2029, or the invisible ranges, so a hostile
52
+ * body would also reach an agent's context with live escape sequences intact.
53
+ *
54
+ * Recursive because the body is arbitrary server JSON — GitLab nests its messages under
55
+ * `message` and `error` in shapes that vary by endpoint — so guarding only a top-level string
56
+ * would miss the common case.
57
+ */
58
+ export function sanitiseDeep(value, secrets, depth = 0) {
59
+ // The body comes from the instance, so the walk is bounded rather than trusted to be shallow.
60
+ if (depth > 12)
61
+ return undefined;
62
+ if (typeof value === 'string')
63
+ return stripControl(redactSecrets(value, secrets));
64
+ if (Array.isArray(value))
65
+ return value.map((item) => sanitiseDeep(item, secrets, depth + 1));
66
+ if (typeof value === 'object' && value !== null) {
67
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [stripControl(key), sanitiseDeep(item, secrets, depth + 1)]));
68
+ }
69
+ return value;
70
+ }
71
+ //# sourceMappingURL=redaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redaction.js","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAEzC,4FAA4F;AAC5F,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,cAAc,CAAU,CAAC;AAEpD,wFAAwF;AACxF,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAE5B;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,MAAoC;IACjE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,IAAI,iBAAiB;YAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACnF,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,YAAY,CAAC,MAAe,OAAO,CAAC,GAAG;IACrD,OAAO,cAAc,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC7D,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,aAAa,CAAC,OAAe,EAAE,OAAyB;IACtE,IAAI,QAAQ,GAAG,OAAO,CAAC;IACvB,KAAK,MAAM,MAAM,IAAI,OAAO;QAAE,QAAQ,GAAG,QAAQ,CAAC,UAAU,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACnF,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,KAAc,EAAE,OAAyB,EAAE,KAAK,GAAG,CAAC;IAC/E,8FAA8F;IAC9F,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,SAAS,CAAC;IACjC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,YAAY,CAAC,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;IAClF,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;IAC7F,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CACxG,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,50 @@
1
+ import type { WriteFileInput } from './gitlab-client.js';
2
+ /** The envelope GitLab answers a file read with; `content` is base64 unless it says otherwise. */
3
+ export interface FilePayload {
4
+ readonly file_name?: string;
5
+ readonly file_path?: string;
6
+ readonly size?: number;
7
+ readonly encoding?: string;
8
+ readonly content?: string;
9
+ readonly ref?: string;
10
+ readonly blob_id?: string;
11
+ readonly commit_id?: string;
12
+ readonly last_commit_id?: string;
13
+ }
14
+ /** A decoded file, and whether decoding produced something a terminal can show. */
15
+ export interface DecodedFile {
16
+ readonly text: string;
17
+ readonly bytes: number;
18
+ /**
19
+ * False when the file is binary. A caller then has the bytes but must not print them: a PNG
20
+ * written to a terminal emits escape sequences, and written to an agent's context it is noise
21
+ * that can carry an instruction.
22
+ */
23
+ readonly printable: boolean;
24
+ }
25
+ /**
26
+ * Decodes the base64 body of a file read.
27
+ *
28
+ * GitLab has answered with `encoding: "text"` on some self-managed versions, so the encoding is
29
+ * honoured rather than assumed — decoding plain text as base64 yields silent garbage, which is
30
+ * exactly the failure a user cannot diagnose from the output.
31
+ */
32
+ export declare function decodeFileContent(payload: FilePayload): DecodedFile;
33
+ export interface FileWriteInput {
34
+ readonly branch?: string;
35
+ readonly content?: string;
36
+ readonly contentFile?: string;
37
+ readonly message?: string;
38
+ readonly authorEmail?: string;
39
+ readonly authorName?: string;
40
+ readonly startBranch?: string;
41
+ readonly lastCommitId?: string;
42
+ }
43
+ /**
44
+ * Turns the flags (or tool inputs) of a file write into the client's input.
45
+ *
46
+ * Content comes from either a literal or a file, never both, and the branch and commit message
47
+ * are required by GitLab — checked here so the refusal names the missing flag instead of arriving
48
+ * as a 400 whose body says `branch is missing`.
49
+ */
50
+ export declare function buildFileWrite(input: FileWriteInput): WriteFileInput;
@@ -0,0 +1,70 @@
1
+ /*
2
+ * Copyright (c) 2026, SimplySF.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { Buffer } from 'node:buffer';
17
+ import { ConfigError } from './errors.js';
18
+ import { readTextFile } from './json-input.js';
19
+ /** A NUL byte does not occur in valid UTF-8 text, and is the cheapest binary tell there is. */
20
+ function looksBinary(text) {
21
+ return text.includes('\u0000');
22
+ }
23
+ /**
24
+ * Decodes the base64 body of a file read.
25
+ *
26
+ * GitLab has answered with `encoding: "text"` on some self-managed versions, so the encoding is
27
+ * honoured rather than assumed — decoding plain text as base64 yields silent garbage, which is
28
+ * exactly the failure a user cannot diagnose from the output.
29
+ */
30
+ export function decodeFileContent(payload) {
31
+ const content = payload.content;
32
+ if (typeof content !== 'string') {
33
+ throw new ConfigError('The response carried no file content. Check the ref and the file path.');
34
+ }
35
+ const buffer = payload.encoding === 'base64' ? Buffer.from(content, 'base64') : Buffer.from(content, 'utf8');
36
+ const text = buffer.toString('utf8');
37
+ return { text, bytes: buffer.byteLength, printable: !looksBinary(text) };
38
+ }
39
+ /**
40
+ * Turns the flags (or tool inputs) of a file write into the client's input.
41
+ *
42
+ * Content comes from either a literal or a file, never both, and the branch and commit message
43
+ * are required by GitLab — checked here so the refusal names the missing flag instead of arriving
44
+ * as a 400 whose body says `branch is missing`.
45
+ */
46
+ export function buildFileWrite(input) {
47
+ if (input.content !== undefined && input.contentFile !== undefined) {
48
+ throw new ConfigError('Pass --content or --content-file, not both.');
49
+ }
50
+ const content = input.content ?? (input.contentFile === undefined ? undefined : readTextFile(input.contentFile, 'Content file'));
51
+ if (content === undefined) {
52
+ throw new ConfigError('File content is required: pass --content or --content-file.');
53
+ }
54
+ if (input.branch === undefined || input.branch === '') {
55
+ throw new ConfigError('A branch is required: pass --branch.');
56
+ }
57
+ if (input.message === undefined || input.message === '') {
58
+ throw new ConfigError('A commit message is required: pass --message.');
59
+ }
60
+ return {
61
+ branch: input.branch,
62
+ content,
63
+ commitMessage: input.message,
64
+ authorEmail: input.authorEmail,
65
+ authorName: input.authorName,
66
+ startBranch: input.startBranch,
67
+ lastCommitId: input.lastCommitId,
68
+ };
69
+ }
70
+ //# sourceMappingURL=repository-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository-files.js","sourceRoot":"","sources":["../src/repository-files.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AA4B/C,+FAA+F;AAC/F,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACjC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAoB;IACpD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAChC,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAChC,MAAM,IAAI,WAAW,CAAC,wEAAwE,CAAC,CAAC;IAClG,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC7G,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACrC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,UAAU,EAAE,SAAS,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;AAC3E,CAAC;AAaD;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,KAAqB;IAClD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACnE,MAAM,IAAI,WAAW,CAAC,6CAA6C,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,OAAO,GACX,KAAK,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC,CAAC;IACnH,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,IAAI,WAAW,CAAC,6DAA6D,CAAC,CAAC;IACvF,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QACtD,MAAM,IAAI,WAAW,CAAC,sCAAsC,CAAC,CAAC;IAChE,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,KAAK,EAAE,EAAE,CAAC;QACxD,MAAM,IAAI,WAAW,CAAC,+CAA+C,CAAC,CAAC;IACzE,CAAC;IAED,OAAO;QACL,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,OAAO;QACP,aAAa,EAAE,KAAK,CAAC,OAAO;QAC5B,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,YAAY,EAAE,KAAK,CAAC,YAAY;KACjC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { SearchScope } from './gitlab-client.js';
2
+ import type { Column } from './output.js';
3
+ import { type Row } from './tables.js';
4
+ /** The scopes every GitLab instance can search without Advanced Search (Elasticsearch) enabled. */
5
+ export declare const BASIC_SCOPES: readonly SearchScope[];
6
+ /**
7
+ * The scopes that need Advanced Search on the instance. Named so a caller can be told *why* a
8
+ * scope came back empty: GitLab answers an unsupported scope with an empty list and a 200, which
9
+ * is indistinguishable from "no matches" unless something says otherwise.
10
+ */
11
+ export declare const ADVANCED_SCOPES: readonly SearchScope[];
12
+ export declare function needsAdvancedSearch(scope: SearchScope): boolean;
13
+ /**
14
+ * The columns to render for a search result set.
15
+ *
16
+ * Each scope returns a different object — a blob hit has a path and a line number, a user hit has
17
+ * a username — so one column set for all of them would show an em dash in most cells. The scope
18
+ * is known before the request, so the renderer picks by scope rather than sniffing the rows.
19
+ */
20
+ export declare function searchColumns(scope: SearchScope): ReadonlyArray<Column<Row>>;
package/lib/search.js ADDED
@@ -0,0 +1,92 @@
1
+ /*
2
+ * Copyright (c) 2026, SimplySF.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { commitColumns, mergeRequestColumns, projectColumns } from './tables.js';
17
+ /** The scopes every GitLab instance can search without Advanced Search (Elasticsearch) enabled. */
18
+ export const BASIC_SCOPES = ['projects', 'issues', 'merge_requests', 'milestones', 'users'];
19
+ /**
20
+ * The scopes that need Advanced Search on the instance. Named so a caller can be told *why* a
21
+ * scope came back empty: GitLab answers an unsupported scope with an empty list and a 200, which
22
+ * is indistinguishable from "no matches" unless something says otherwise.
23
+ */
24
+ export const ADVANCED_SCOPES = ['blobs', 'commits', 'wiki_blobs', 'notes', 'snippet_titles'];
25
+ export function needsAdvancedSearch(scope) {
26
+ return ADVANCED_SCOPES.includes(scope);
27
+ }
28
+ const issueColumns = [
29
+ { header: 'IID', value: (row) => row.iid },
30
+ { header: 'STATE', value: (row) => row.state },
31
+ { header: 'PROJECT', value: (row) => row.project_id },
32
+ { header: 'TITLE', value: (row) => row.title },
33
+ ];
34
+ const blobColumns = [
35
+ { header: 'PATH', value: (row) => row.path ?? row.filename },
36
+ { header: 'REF', value: (row) => row.ref },
37
+ { header: 'LINE', value: (row) => row.startline },
38
+ { header: 'PROJECT', value: (row) => row.project_id },
39
+ ];
40
+ const userColumns = [
41
+ { header: 'ID', value: (row) => row.id },
42
+ { header: 'USERNAME', value: (row) => row.username },
43
+ { header: 'NAME', value: (row) => row.name },
44
+ { header: 'STATE', value: (row) => row.state },
45
+ ];
46
+ const milestoneColumns = [
47
+ { header: 'IID', value: (row) => row.iid },
48
+ { header: 'STATE', value: (row) => row.state },
49
+ { header: 'DUE', value: (row) => row.due_date },
50
+ { header: 'TITLE', value: (row) => row.title },
51
+ ];
52
+ const noteColumns = [
53
+ { header: 'ID', value: (row) => row.id },
54
+ { header: 'AUTHOR', value: (row) => row.author?.username },
55
+ { header: 'BODY', value: (row) => row.body },
56
+ ];
57
+ const snippetColumns = [
58
+ { header: 'ID', value: (row) => row.id },
59
+ { header: 'TITLE', value: (row) => row.title },
60
+ { header: 'FILE', value: (row) => row.file_name },
61
+ ];
62
+ /**
63
+ * The columns to render for a search result set.
64
+ *
65
+ * Each scope returns a different object — a blob hit has a path and a line number, a user hit has
66
+ * a username — so one column set for all of them would show an em dash in most cells. The scope
67
+ * is known before the request, so the renderer picks by scope rather than sniffing the rows.
68
+ */
69
+ export function searchColumns(scope) {
70
+ switch (scope) {
71
+ case 'projects':
72
+ return projectColumns;
73
+ case 'issues':
74
+ return issueColumns;
75
+ case 'merge_requests':
76
+ return mergeRequestColumns;
77
+ case 'milestones':
78
+ return milestoneColumns;
79
+ case 'commits':
80
+ return commitColumns;
81
+ case 'blobs':
82
+ case 'wiki_blobs':
83
+ return blobColumns;
84
+ case 'notes':
85
+ return noteColumns;
86
+ case 'users':
87
+ return userColumns;
88
+ case 'snippet_titles':
89
+ return snippetColumns;
90
+ }
91
+ }
92
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { Column } from './output.js';
2
+ /**
3
+ * The column sets each list command renders.
4
+ *
5
+ * They live here, in core, rather than on the commands for the same reason the operations do:
6
+ * the MCP server's tool descriptions quote what a list actually contains, and a column set that
7
+ * only the CLI knew about would drift from that description with nothing to catch it. Every row
8
+ * type is `Record<string, unknown>` because these are raw GitLab payloads — the API adds fields
9
+ * between releases, and typing them exhaustively would make an added field a compile error
10
+ * rather than a non-event.
11
+ */
12
+ export type Row = Record<string, unknown>;
13
+ export declare const projectColumns: ReadonlyArray<Column<Row>>;
14
+ export declare const branchColumns: ReadonlyArray<Column<Row>>;
15
+ export declare const tagColumns: ReadonlyArray<Column<Row>>;
16
+ export declare const commitColumns: ReadonlyArray<Column<Row>>;
17
+ export declare const mergeRequestColumns: ReadonlyArray<Column<Row>>;
18
+ export declare const jobColumns: ReadonlyArray<Column<Row>>;
19
+ export declare const variableColumns: ReadonlyArray<Column<Row>>;
20
+ export declare const environmentColumns: ReadonlyArray<Column<Row>>;
21
+ export declare const releaseColumns: ReadonlyArray<Column<Row>>;
package/lib/tables.js ADDED
@@ -0,0 +1,105 @@
1
+ /*
2
+ * Copyright (c) 2026, SimplySF.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /** Reads a nested field without asserting a shape on server JSON. */
17
+ function at(row, ...path) {
18
+ let cursor = row;
19
+ for (const key of path) {
20
+ if (typeof cursor !== 'object' || cursor === null)
21
+ return undefined;
22
+ cursor = cursor[key];
23
+ }
24
+ return cursor;
25
+ }
26
+ /**
27
+ * Renders a boolean API field.
28
+ *
29
+ * Deliberately 'no' rather than a blank: the renderer shows a missing value as an em dash, and a
30
+ * branch that is genuinely not merged is not a missing value. Anything that is not a boolean is
31
+ * left undefined so it renders as the em dash, which is then honest — the API did not say.
32
+ */
33
+ function flag(value) {
34
+ return typeof value === 'boolean' ? (value ? 'yes' : 'no') : undefined;
35
+ }
36
+ /** Trims an ISO timestamp to the date, which is what a list is actually scanned for. */
37
+ function day(value) {
38
+ return typeof value === 'string' && value.length >= 10 ? value.slice(0, 10) : value;
39
+ }
40
+ export const projectColumns = [
41
+ { header: 'ID', value: (row) => row.id },
42
+ { header: 'PATH', value: (row) => row.path_with_namespace },
43
+ { header: 'VISIBILITY', value: (row) => row.visibility },
44
+ { header: 'DEFAULT BRANCH', value: (row) => row.default_branch },
45
+ { header: 'LAST ACTIVITY', value: (row) => day(row.last_activity_at) },
46
+ ];
47
+ export const branchColumns = [
48
+ { header: 'NAME', value: (row) => row.name },
49
+ { header: 'DEFAULT', value: (row) => flag(row.default) },
50
+ { header: 'PROTECTED', value: (row) => flag(row.protected) },
51
+ { header: 'MERGED', value: (row) => flag(row.merged) },
52
+ { header: 'COMMIT', value: (row) => at(row, 'commit', 'short_id') },
53
+ { header: 'TITLE', value: (row) => at(row, 'commit', 'title') },
54
+ ];
55
+ export const tagColumns = [
56
+ { header: 'NAME', value: (row) => row.name },
57
+ { header: 'COMMIT', value: (row) => at(row, 'commit', 'short_id') },
58
+ { header: 'CREATED', value: (row) => day(at(row, 'commit', 'created_at')) },
59
+ { header: 'MESSAGE', value: (row) => row.message },
60
+ ];
61
+ export const commitColumns = [
62
+ { header: 'SHA', value: (row) => row.short_id },
63
+ { header: 'DATE', value: (row) => day(row.created_at) },
64
+ { header: 'AUTHOR', value: (row) => row.author_name },
65
+ { header: 'TITLE', value: (row) => row.title },
66
+ ];
67
+ export const mergeRequestColumns = [
68
+ { header: 'IID', value: (row) => row.iid },
69
+ { header: 'STATE', value: (row) => row.state },
70
+ { header: 'SOURCE', value: (row) => row.source_branch },
71
+ { header: 'TARGET', value: (row) => row.target_branch },
72
+ { header: 'AUTHOR', value: (row) => at(row, 'author', 'username') },
73
+ { header: 'UPDATED', value: (row) => day(row.updated_at) },
74
+ { header: 'TITLE', value: (row) => row.title },
75
+ ];
76
+ export const jobColumns = [
77
+ { header: 'ID', value: (row) => row.id },
78
+ { header: 'STATUS', value: (row) => row.status },
79
+ { header: 'STAGE', value: (row) => row.stage },
80
+ { header: 'NAME', value: (row) => row.name },
81
+ { header: 'REF', value: (row) => row.ref },
82
+ { header: 'PIPELINE', value: (row) => at(row, 'pipeline', 'id') },
83
+ ];
84
+ export const variableColumns = [
85
+ { header: 'KEY', value: (row) => row.key },
86
+ { header: 'SCOPE', value: (row) => row.environment_scope },
87
+ { header: 'TYPE', value: (row) => row.variable_type },
88
+ { header: 'PROTECTED', value: (row) => flag(row.protected) },
89
+ { header: 'MASKED', value: (row) => flag(row.masked) },
90
+ { header: 'VALUE', value: (row) => row.value },
91
+ ];
92
+ export const environmentColumns = [
93
+ { header: 'ID', value: (row) => row.id },
94
+ { header: 'NAME', value: (row) => row.name },
95
+ { header: 'STATE', value: (row) => row.state },
96
+ { header: 'TIER', value: (row) => row.tier },
97
+ { header: 'EXTERNAL URL', value: (row) => row.external_url },
98
+ ];
99
+ export const releaseColumns = [
100
+ { header: 'TAG', value: (row) => row.tag_name },
101
+ { header: 'NAME', value: (row) => row.name },
102
+ { header: 'RELEASED', value: (row) => day(row.released_at) },
103
+ { header: 'AUTHOR', value: (row) => at(row, 'author', 'username') },
104
+ ];
105
+ //# sourceMappingURL=tables.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tables.js","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { type IncomingMessage, type ServerResponse } from 'node:http';
2
+ export interface RecordedRequest {
3
+ method: string;
4
+ url: string;
5
+ body: string;
6
+ }
7
+ export type RouteHandler = (request: IncomingMessage, response: ServerResponse, body: string) => void;
8
+ export interface TestServer {
9
+ baseUrl: string;
10
+ requests: RecordedRequest[];
11
+ route: (pathname: string, handler: RouteHandler) => void;
12
+ close: () => Promise<void>;
13
+ }
14
+ export declare function respondJson(response: ServerResponse, status: number, payload: unknown, headers?: Record<string, string>): void;
15
+ /** Tiny local HTTP server: register handlers per pathname, and every request gets recorded. */
16
+ export declare function startTestServer(): Promise<TestServer>;
package/lib/testing.js ADDED
@@ -0,0 +1,63 @@
1
+ /*
2
+ * Copyright (c) 2026, SimplySF.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { createServer } from 'node:http';
17
+ export function respondJson(response, status, payload, headers = {}) {
18
+ response.writeHead(status, { 'content-type': 'application/json', ...headers });
19
+ response.end(JSON.stringify(payload));
20
+ }
21
+ /** Tiny local HTTP server: register handlers per pathname, and every request gets recorded. */
22
+ export async function startTestServer() {
23
+ const routes = new Map();
24
+ const requests = [];
25
+ const server = createServer((request, response) => {
26
+ const chunks = [];
27
+ request.on('data', (chunk) => chunks.push(chunk));
28
+ request.on('end', () => {
29
+ const body = Buffer.concat(chunks).toString('utf8');
30
+ const url = new URL(request.url ?? '/', 'http://127.0.0.1');
31
+ requests.push({ method: request.method ?? '', url: request.url ?? '', body });
32
+ const handler = routes.get(url.pathname);
33
+ if (handler) {
34
+ handler(request, response, body);
35
+ }
36
+ else {
37
+ respondJson(response, 404, { error: `no route for ${url.pathname}` });
38
+ }
39
+ });
40
+ });
41
+ const baseUrl = await new Promise((resolve, reject) => {
42
+ server.once('error', reject);
43
+ server.listen(0, '127.0.0.1', () => {
44
+ const address = server.address();
45
+ if (!address || typeof address === 'string') {
46
+ reject(new Error('failed to allocate a port'));
47
+ return;
48
+ }
49
+ resolve(`http://127.0.0.1:${address.port}`);
50
+ });
51
+ });
52
+ return {
53
+ baseUrl,
54
+ requests,
55
+ route: (pathname, handler) => {
56
+ routes.set(pathname, handler);
57
+ },
58
+ close: () => new Promise((resolve, reject) => {
59
+ server.close((error) => (error ? reject(error) : resolve()));
60
+ }),
61
+ };
62
+ }
63
+ //# sourceMappingURL=testing.js.map