@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.
- package/README.md +76 -0
- package/lib/auth.d.ts +9 -0
- package/lib/auth.js +26 -0
- package/lib/auth.js.map +1 -0
- package/lib/cicd.d.ts +29 -0
- package/lib/cicd.js +56 -0
- package/lib/cicd.js.map +1 -0
- package/lib/commits.d.ts +34 -0
- package/lib/commits.js +101 -0
- package/lib/commits.js.map +1 -0
- package/lib/config.d.ts +21 -0
- package/lib/config.js +76 -0
- package/lib/config.js.map +1 -0
- package/lib/env-file.d.ts +11 -0
- package/lib/env-file.js +98 -0
- package/lib/env-file.js.map +1 -0
- package/lib/errors.d.ts +31 -0
- package/lib/errors.js +66 -0
- package/lib/errors.js.map +1 -0
- package/lib/gitlab-client.d.ts +137 -0
- package/lib/gitlab-client.js +250 -0
- package/lib/gitlab-client.js.map +1 -0
- package/lib/http.d.ts +82 -0
- package/lib/http.js +285 -0
- package/lib/http.js.map +1 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +46 -0
- package/lib/index.js.map +1 -0
- package/lib/json-input.d.ts +28 -0
- package/lib/json-input.js +84 -0
- package/lib/json-input.js.map +1 -0
- package/lib/merge-requests.d.ts +40 -0
- package/lib/merge-requests.js +89 -0
- package/lib/merge-requests.js.map +1 -0
- package/lib/output.d.ts +14 -0
- package/lib/output.js +65 -0
- package/lib/output.js.map +1 -0
- package/lib/redaction.d.ts +26 -0
- package/lib/redaction.js +71 -0
- package/lib/redaction.js.map +1 -0
- package/lib/repository-files.d.ts +50 -0
- package/lib/repository-files.js +70 -0
- package/lib/repository-files.js.map +1 -0
- package/lib/search.d.ts +20 -0
- package/lib/search.js +92 -0
- package/lib/search.js.map +1 -0
- package/lib/tables.d.ts +21 -0
- package/lib/tables.js +105 -0
- package/lib/tables.js.map +1 -0
- package/lib/testing.d.ts +16 -0
- package/lib/testing.js +63 -0
- package/lib/testing.js.map +1 -0
- package/lib/text.d.ts +10 -0
- package/lib/text.js +80 -0
- package/lib/text.js.map +1 -0
- package/lib/write-safety.d.ts +19 -0
- package/lib/write-safety.js +45 -0
- package/lib/write-safety.js.map +1 -0
- package/package.json +130 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @simplysf/simply-gitlab-core
|
|
2
|
+
|
|
3
|
+
Configuration, authentication, the HTTP client, and the shared logic that
|
|
4
|
+
[`@simplysf/simply-gitlab`](../simply-gitlab) (the CLI) and
|
|
5
|
+
[`@simplysf/simply-gitlab-mcp`](../simply-gitlab-mcp) (the MCP server) are both built on.
|
|
6
|
+
|
|
7
|
+
Everything a command does between parsing its input and rendering its result lives here, so the CLI
|
|
8
|
+
and the MCP server cannot disagree about what a rule means. Nothing in this package touches a
|
|
9
|
+
terminal or a process: no oclif, no child processes, no `process.argv`, nothing written to stdout or
|
|
10
|
+
stderr. `process.env` is read only through an injectable `env` parameter.
|
|
11
|
+
|
|
12
|
+
You want this package directly if you are building something else against GitLab in TypeScript. If
|
|
13
|
+
you want a CLI or an MCP server, install one of those instead.
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install @simplysf/simply-gitlab-core
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { GitLabClient, resolveGitLabConfig } from '@simplysf/simply-gitlab-core';
|
|
23
|
+
|
|
24
|
+
// Reads GITLAB_URL (default https://gitlab.com) and GITLAB_TOKEN, and throws a ConfigError
|
|
25
|
+
// naming what is missing.
|
|
26
|
+
const client = new GitLabClient(resolveGitLabConfig());
|
|
27
|
+
|
|
28
|
+
// A project is named by numeric id or by full path; the path is encoded for you.
|
|
29
|
+
const project = await client.getProject('group/subgroup/project');
|
|
30
|
+
|
|
31
|
+
// Lists follow pages up to your limit and tell you whether they got everything.
|
|
32
|
+
const { items, total, complete } = await client.listMergeRequests('group/project', { state: 'opened' }, 50);
|
|
33
|
+
if (!complete) console.warn(`showing ${items.length} of ${total ?? 'many'}`);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## What is in here
|
|
37
|
+
|
|
38
|
+
**Configuration and auth** — `resolveGitLabConfig`, `buildAuthHeaders`, `loadEnvFile`.
|
|
39
|
+
|
|
40
|
+
**Transport** — `HttpTransport`, with retries on 429 and 5xx, a capped `Retry-After`, a 30s deadline
|
|
41
|
+
covering the response body, GitLab's bracketed array query parameters, a non-JSON mode for job
|
|
42
|
+
traces, and its offset-pagination headers.
|
|
43
|
+
|
|
44
|
+
**The client** — `GitLabClient`, which owns project-path encoding and page walking, and returns
|
|
45
|
+
`{ items, total?, pages, complete }` from every list.
|
|
46
|
+
|
|
47
|
+
**Errors** — `ConfigError` (exit 2), `AuthError` (3), `NetworkError` (1), `HttpError` (1), all
|
|
48
|
+
extending `CliError`.
|
|
49
|
+
|
|
50
|
+
**Safety** — `assertWritesAllowed`/`isReadOnly` for `GITLAB_READ_ONLY`; `redactSecrets` and
|
|
51
|
+
`sanitiseDeep` to keep a token out of any message or response body; `stripControl` and
|
|
52
|
+
`stripControlOneLine` for text the instance chose.
|
|
53
|
+
|
|
54
|
+
**Operations** — `assertIid`, `buildMergeRequestCreateBody`, `buildMergeRequestUpdateBody`,
|
|
55
|
+
`buildCommitBody`, `parseCommitActions`, `buildFileWrite`, `decodeFileContent`, `maskVariables`,
|
|
56
|
+
`tailLog`, `searchColumns`.
|
|
57
|
+
|
|
58
|
+
**Rendering** — `formatTable`, `formatKeyValue`, and the column sets each list command uses.
|
|
59
|
+
|
|
60
|
+
**Testing** — `@simplysf/simply-gitlab-core/testing` exports `startTestServer` and `respondJson`, a
|
|
61
|
+
tiny local HTTP server that records every request. Both other packages test against it.
|
|
62
|
+
|
|
63
|
+
## Stability
|
|
64
|
+
|
|
65
|
+
Every export from `src/index.ts` is semver-covered: adding one is a minor or patch change, removing
|
|
66
|
+
or renaming one is breaking. `test/index.test.ts` pins the list, so a removal fails a test rather
|
|
67
|
+
than shipping quietly in a patch.
|
|
68
|
+
|
|
69
|
+
## Documentation
|
|
70
|
+
|
|
71
|
+
- [GitLab client core design](../../docs/design/0001-gitlab-client-core.md)
|
|
72
|
+
- [Contributing](./CONTRIBUTING.md)
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
Apache-2.0.
|
package/lib/auth.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GitLabConfig } from './config.js';
|
|
2
|
+
/**
|
|
3
|
+
* GitLab accepts the same token as `PRIVATE-TOKEN` or as an OAuth bearer, and the two are not
|
|
4
|
+
* interchangeable: a personal, project, or group access token authenticates only through
|
|
5
|
+
* `PRIVATE-TOKEN`, while `Authorization: Bearer` is reserved for OAuth 2 tokens. This CLI issues
|
|
6
|
+
* neither OAuth flow, so it sends the header that works for every token a user can create by
|
|
7
|
+
* hand. Returns just the header so callers can merge it into a request.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildAuthHeaders(config: GitLabConfig): Record<string, string>;
|
package/lib/auth.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
/**
|
|
17
|
+
* GitLab accepts the same token as `PRIVATE-TOKEN` or as an OAuth bearer, and the two are not
|
|
18
|
+
* interchangeable: a personal, project, or group access token authenticates only through
|
|
19
|
+
* `PRIVATE-TOKEN`, while `Authorization: Bearer` is reserved for OAuth 2 tokens. This CLI issues
|
|
20
|
+
* neither OAuth flow, so it sends the header that works for every token a user can create by
|
|
21
|
+
* hand. Returns just the header so callers can merge it into a request.
|
|
22
|
+
*/
|
|
23
|
+
export function buildAuthHeaders(config) {
|
|
24
|
+
return { 'PRIVATE-TOKEN': config.token };
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=auth.js.map
|
package/lib/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAoB;IACnD,OAAO,EAAE,eAAe,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;AAC3C,CAAC"}
|
package/lib/cicd.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** What a hidden CI variable's value is replaced with, in both the table and the JSON. */
|
|
2
|
+
export declare const HIDDEN = "<hidden>";
|
|
3
|
+
/**
|
|
4
|
+
* Replaces the value of every CI/CD variable with a placeholder.
|
|
5
|
+
*
|
|
6
|
+
* `GET /projects/:id/variables` returns every variable's value in clear text — including the ones
|
|
7
|
+
* marked "masked", because masking only applies to job logs. Those values are deploy keys, signing
|
|
8
|
+
* certificates, and production credentials, and the endpoint's usual caller is a person listing
|
|
9
|
+
* variables to see *which* exist, not to read them.
|
|
10
|
+
*
|
|
11
|
+
* So the values are hidden by default and revealed only on an explicit request. This is the one
|
|
12
|
+
* place the CLI and the MCP server deliberately differ: `--reveal` exists on the command, and
|
|
13
|
+
* there is no equivalent tool input, because a value revealed to an agent is a value copied into
|
|
14
|
+
* a context window, a transcript, and whatever the agent writes next. A person who needs the
|
|
15
|
+
* value can run the command; nothing about the tool catalogue should make exfiltrating a
|
|
16
|
+
* production secret a one-call operation.
|
|
17
|
+
*/
|
|
18
|
+
export declare function maskVariables(items: readonly unknown[]): unknown[];
|
|
19
|
+
/**
|
|
20
|
+
* Trims a job trace to its last `lines` lines.
|
|
21
|
+
*
|
|
22
|
+
* A trace is regularly tens of megabytes, and the interesting part of a failed job is at the end.
|
|
23
|
+
* Returning the whole thing by default would blow a terminal's scrollback and an agent's context
|
|
24
|
+
* for a question that is almost always "why did it fail".
|
|
25
|
+
*/
|
|
26
|
+
export declare function tailLog(log: string, lines: number): {
|
|
27
|
+
readonly text: string;
|
|
28
|
+
readonly truncated: boolean;
|
|
29
|
+
};
|
package/lib/cicd.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
/** What a hidden CI variable's value is replaced with, in both the table and the JSON. */
|
|
17
|
+
export const HIDDEN = '<hidden>';
|
|
18
|
+
/**
|
|
19
|
+
* Replaces the value of every CI/CD variable with a placeholder.
|
|
20
|
+
*
|
|
21
|
+
* `GET /projects/:id/variables` returns every variable's value in clear text — including the ones
|
|
22
|
+
* marked "masked", because masking only applies to job logs. Those values are deploy keys, signing
|
|
23
|
+
* certificates, and production credentials, and the endpoint's usual caller is a person listing
|
|
24
|
+
* variables to see *which* exist, not to read them.
|
|
25
|
+
*
|
|
26
|
+
* So the values are hidden by default and revealed only on an explicit request. This is the one
|
|
27
|
+
* place the CLI and the MCP server deliberately differ: `--reveal` exists on the command, and
|
|
28
|
+
* there is no equivalent tool input, because a value revealed to an agent is a value copied into
|
|
29
|
+
* a context window, a transcript, and whatever the agent writes next. A person who needs the
|
|
30
|
+
* value can run the command; nothing about the tool catalogue should make exfiltrating a
|
|
31
|
+
* production secret a one-call operation.
|
|
32
|
+
*/
|
|
33
|
+
export function maskVariables(items) {
|
|
34
|
+
return items.map((item) => {
|
|
35
|
+
if (typeof item !== 'object' || item === null)
|
|
36
|
+
return item;
|
|
37
|
+
const variable = item;
|
|
38
|
+
return 'value' in variable ? { ...variable, value: HIDDEN } : variable;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Trims a job trace to its last `lines` lines.
|
|
43
|
+
*
|
|
44
|
+
* A trace is regularly tens of megabytes, and the interesting part of a failed job is at the end.
|
|
45
|
+
* Returning the whole thing by default would blow a terminal's scrollback and an agent's context
|
|
46
|
+
* for a question that is almost always "why did it fail".
|
|
47
|
+
*/
|
|
48
|
+
export function tailLog(log, lines) {
|
|
49
|
+
if (!Number.isInteger(lines) || lines < 1)
|
|
50
|
+
return { text: log, truncated: false };
|
|
51
|
+
const all = log.split('\n');
|
|
52
|
+
if (all.length <= lines)
|
|
53
|
+
return { text: log, truncated: false };
|
|
54
|
+
return { text: all.slice(-lines).join('\n'), truncated: true };
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=cicd.js.map
|
package/lib/cicd.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cicd.js","sourceRoot":"","sources":["../src/cicd.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,0FAA0F;AAC1F,MAAM,CAAC,MAAM,MAAM,GAAG,UAAU,CAAC;AAEjC;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,aAAa,CAAC,KAAyB;IACrD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACxB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAC3D,MAAM,QAAQ,GAAG,IAA+B,CAAC;QACjD,OAAO,OAAO,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;IACzE,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CAAC,GAAW,EAAE,KAAa;IAChD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IAClF,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,GAAG,CAAC,MAAM,IAAI,KAAK;QAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IAChE,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AACjE,CAAC"}
|
package/lib/commits.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CommitAction } from './gitlab-client.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validates the `actions` array of a commit.
|
|
4
|
+
*
|
|
5
|
+
* GitLab answers a malformed action with a 400 whose body names neither the index nor the file,
|
|
6
|
+
* so a ten-file commit with one bad entry becomes a hunt. Every rule checked here is one GitLab
|
|
7
|
+
* itself enforces — this only moves the report to where the caller can act on it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseCommitActions(value: unknown): readonly CommitAction[];
|
|
10
|
+
export interface CommitInput {
|
|
11
|
+
readonly branch?: string;
|
|
12
|
+
readonly message?: string;
|
|
13
|
+
readonly actions?: unknown;
|
|
14
|
+
readonly startBranch?: string;
|
|
15
|
+
readonly startSha?: string;
|
|
16
|
+
readonly authorEmail?: string;
|
|
17
|
+
readonly authorName?: string;
|
|
18
|
+
readonly force?: boolean;
|
|
19
|
+
readonly stats?: boolean;
|
|
20
|
+
readonly body?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Builds the body of a multi-file commit. Typed properties are merged over `body`, so a template
|
|
24
|
+
* file can supply the action list while a flag names the branch.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildCommitBody(input: CommitInput): Record<string, unknown>;
|
|
27
|
+
/**
|
|
28
|
+
* Parses the `--actions` flag.
|
|
29
|
+
*
|
|
30
|
+
* `JSON.parse` on its own raises a SyntaxError carrying a snippet of the input, which for a flag
|
|
31
|
+
* a caller may have built from a file would echo that file's first bytes into the error. Only the
|
|
32
|
+
* position is kept, matching how `parseBodyInput` handles the same risk.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseActionsInput(raw: string | undefined): unknown;
|
package/lib/commits.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
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 { ConfigError } from './errors.js';
|
|
17
|
+
import { mergeBody } from './json-input.js';
|
|
18
|
+
const ACTIONS = new Set(['create', 'delete', 'move', 'update', 'chmod']);
|
|
19
|
+
/** What each action needs beyond `file_path`, checked before the request rather than after. */
|
|
20
|
+
const REQUIRED = {
|
|
21
|
+
create: ['content'],
|
|
22
|
+
update: ['content'],
|
|
23
|
+
move: ['previous_path'],
|
|
24
|
+
delete: [],
|
|
25
|
+
chmod: ['execute_filemode'],
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Validates the `actions` array of a commit.
|
|
29
|
+
*
|
|
30
|
+
* GitLab answers a malformed action with a 400 whose body names neither the index nor the file,
|
|
31
|
+
* so a ten-file commit with one bad entry becomes a hunt. Every rule checked here is one GitLab
|
|
32
|
+
* itself enforces — this only moves the report to where the caller can act on it.
|
|
33
|
+
*/
|
|
34
|
+
export function parseCommitActions(value) {
|
|
35
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
36
|
+
throw new ConfigError('A commit needs at least one action, as a JSON array under "actions".');
|
|
37
|
+
}
|
|
38
|
+
return value.map((raw, index) => {
|
|
39
|
+
const where = `actions[${index}]`;
|
|
40
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
41
|
+
throw new ConfigError(`${where} must be a JSON object.`);
|
|
42
|
+
}
|
|
43
|
+
const action = raw;
|
|
44
|
+
const verb = action.action;
|
|
45
|
+
if (typeof verb !== 'string' || !ACTIONS.has(verb)) {
|
|
46
|
+
throw new ConfigError(`${where}.action must be one of ${[...ACTIONS].join(', ')}.`);
|
|
47
|
+
}
|
|
48
|
+
if (typeof action.file_path !== 'string' || action.file_path === '') {
|
|
49
|
+
throw new ConfigError(`${where}.file_path is required.`);
|
|
50
|
+
}
|
|
51
|
+
for (const key of REQUIRED[verb] ?? []) {
|
|
52
|
+
if (action[key] === undefined) {
|
|
53
|
+
throw new ConfigError(`${where}.${key} is required for the "${verb}" action.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return action;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Builds the body of a multi-file commit. Typed properties are merged over `body`, so a template
|
|
61
|
+
* file can supply the action list while a flag names the branch.
|
|
62
|
+
*/
|
|
63
|
+
export function buildCommitBody(input) {
|
|
64
|
+
const merged = mergeBody(input.body, {
|
|
65
|
+
branch: input.branch,
|
|
66
|
+
commit_message: input.message,
|
|
67
|
+
actions: input.actions,
|
|
68
|
+
start_branch: input.startBranch,
|
|
69
|
+
start_sha: input.startSha,
|
|
70
|
+
author_email: input.authorEmail,
|
|
71
|
+
author_name: input.authorName,
|
|
72
|
+
force: input.force,
|
|
73
|
+
stats: input.stats,
|
|
74
|
+
});
|
|
75
|
+
if (typeof merged.branch !== 'string' || merged.branch === '') {
|
|
76
|
+
throw new ConfigError('A branch is required: pass --branch.');
|
|
77
|
+
}
|
|
78
|
+
if (typeof merged.commit_message !== 'string' || merged.commit_message === '') {
|
|
79
|
+
throw new ConfigError('A commit message is required: pass --message.');
|
|
80
|
+
}
|
|
81
|
+
return { ...merged, actions: parseCommitActions(merged.actions) };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Parses the `--actions` flag.
|
|
85
|
+
*
|
|
86
|
+
* `JSON.parse` on its own raises a SyntaxError carrying a snippet of the input, which for a flag
|
|
87
|
+
* a caller may have built from a file would echo that file's first bytes into the error. Only the
|
|
88
|
+
* position is kept, matching how `parseBodyInput` handles the same risk.
|
|
89
|
+
*/
|
|
90
|
+
export function parseActionsInput(raw) {
|
|
91
|
+
if (raw === undefined)
|
|
92
|
+
return undefined;
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(raw);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const position = error instanceof Error ? /position \d+(?::? line \d+ column \d+)?/.exec(error.message)?.[0] : undefined;
|
|
98
|
+
throw new ConfigError(`--actions is not valid JSON${position === undefined ? '' : ` at ${position}`}.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=commits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commits.js","sourceRoot":"","sources":["../src/commits.ts"],"names":[],"mappings":"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"}
|
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface GitLabConfig {
|
|
2
|
+
/** Instance origin, with any trailing path preserved for instances served under a subpath. */
|
|
3
|
+
readonly url: string;
|
|
4
|
+
/** Personal, project, or group access token, sent as `PRIVATE-TOKEN`. */
|
|
5
|
+
readonly token: string;
|
|
6
|
+
}
|
|
7
|
+
/** Per-invocation overrides, sourced from global flags. Anything set here beats the environment. */
|
|
8
|
+
export interface ConfigOverrides {
|
|
9
|
+
readonly url?: string;
|
|
10
|
+
readonly token?: string;
|
|
11
|
+
}
|
|
12
|
+
export type EnvLike = Record<string, string | undefined>;
|
|
13
|
+
export declare const URL_ENV = "GITLAB_URL";
|
|
14
|
+
export declare const TOKEN_ENV = "GITLAB_TOKEN";
|
|
15
|
+
/**
|
|
16
|
+
* Where an unconfigured `GITLAB_URL` points. GitLab is one product with one API, and the great
|
|
17
|
+
* majority of tokens are gitlab.com tokens, so requiring the URL would be ceremony for the common
|
|
18
|
+
* case. A self-managed instance sets the variable.
|
|
19
|
+
*/
|
|
20
|
+
export declare const DEFAULT_URL = "https://gitlab.com";
|
|
21
|
+
export declare function resolveGitLabConfig(overrides?: ConfigOverrides, env?: EnvLike): GitLabConfig;
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
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 { ConfigError } from './errors.js';
|
|
18
|
+
export const URL_ENV = 'GITLAB_URL';
|
|
19
|
+
export const TOKEN_ENV = 'GITLAB_TOKEN';
|
|
20
|
+
const SSL_VERIFY_ENV = 'GITLAB_SSL_VERIFY';
|
|
21
|
+
/**
|
|
22
|
+
* Where an unconfigured `GITLAB_URL` points. GitLab is one product with one API, and the great
|
|
23
|
+
* majority of tokens are gitlab.com tokens, so requiring the URL would be ceremony for the common
|
|
24
|
+
* case. A self-managed instance sets the variable.
|
|
25
|
+
*/
|
|
26
|
+
export const DEFAULT_URL = 'https://gitlab.com';
|
|
27
|
+
const FALSE_VALUES = new Set(['false', '0', 'no', 'off']);
|
|
28
|
+
const CA_BUNDLE_HINT = 'NODE_EXTRA_CA_CERTS=/path/to/ca.pem';
|
|
29
|
+
/**
|
|
30
|
+
* Parses the configured base URL. Unlike Atlassian's two deployments, every GitLab instance
|
|
31
|
+
* speaks the same `/api/v4` surface, so the only normalization needed is trimming a trailing
|
|
32
|
+
* slash — and keeping any subpath, because self-managed instances are routinely served from one
|
|
33
|
+
* (`https://example.com/gitlab`).
|
|
34
|
+
*/
|
|
35
|
+
function normalizeUrl(rawUrl) {
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = new URL(rawUrl);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new ConfigError(`GitLab URL is not a valid URL: ${rawUrl}. Set ${URL_ENV} to something like https://gitlab.example.com.`);
|
|
42
|
+
}
|
|
43
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
44
|
+
throw new ConfigError(`GitLab URL must use http or https, got ${parsed.protocol} (${URL_ENV}).`);
|
|
45
|
+
}
|
|
46
|
+
// A pasted project URL — https://gitlab.com/group/project — would otherwise be treated as an
|
|
47
|
+
// instance served under /group/project, and every request would 404 with nothing pointing at
|
|
48
|
+
// the cause. gitlab.com has no subpath, so the path is noise there by construction.
|
|
49
|
+
if (parsed.hostname.toLowerCase() === 'gitlab.com')
|
|
50
|
+
return parsed.origin;
|
|
51
|
+
return `${parsed.origin}${parsed.pathname.replace(/\/+$/, '')}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Certificate verification is not negotiable, so a `GITLAB_SSL_VERIFY=false` carried over from
|
|
55
|
+
* other GitLab tooling fails loudly rather than being silently ignored — the alternative is a
|
|
56
|
+
* user believing verification is off when it is on, or vice versa. The fix keeps verification
|
|
57
|
+
* intact.
|
|
58
|
+
*/
|
|
59
|
+
function rejectSslOptOut(env) {
|
|
60
|
+
const raw = env[SSL_VERIFY_ENV];
|
|
61
|
+
if (raw !== undefined && FALSE_VALUES.has(raw.trim().toLowerCase())) {
|
|
62
|
+
throw new ConfigError(`${SSL_VERIFY_ENV}=${raw} is not supported: this CLI always verifies TLS certificates. ` +
|
|
63
|
+
`If your GitLab instance is behind an internal or agency CA, trust that CA instead with ${CA_BUNDLE_HINT}.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function resolveGitLabConfig(overrides = {}, env = process.env) {
|
|
67
|
+
rejectSslOptOut(env);
|
|
68
|
+
const rawUrl = (overrides.url ?? env[URL_ENV])?.trim();
|
|
69
|
+
const token = (overrides.token ?? env[TOKEN_ENV])?.trim();
|
|
70
|
+
if (!token) {
|
|
71
|
+
throw new ConfigError(`GitLab authentication requires a token. Set ${TOKEN_ENV} or pass --gitlab-token. ` +
|
|
72
|
+
'Create one under Preferences > Access tokens with the api scope (read_api is enough for read-only use).');
|
|
73
|
+
}
|
|
74
|
+
return { url: normalizeUrl(rawUrl === undefined || rawUrl === '' ? DEFAULT_URL : rawUrl), token };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAiB1C,MAAM,CAAC,MAAM,OAAO,GAAG,YAAY,CAAC;AACpC,MAAM,CAAC,MAAM,SAAS,GAAG,cAAc,CAAC;AACxC,MAAM,cAAc,GAAG,mBAAmB,CAAC;AAE3C;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,oBAAoB,CAAC;AAEhD,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;AAC1D,MAAM,cAAc,GAAG,qCAAqC,CAAC;AAE7D;;;;;GAKG;AACH,SAAS,YAAY,CAAC,MAAc;IAClC,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,WAAW,CACnB,kCAAkC,MAAM,SAAS,OAAO,gDAAgD,CACzG,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChE,MAAM,IAAI,WAAW,CAAC,0CAA0C,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,CAAC;IACnG,CAAC;IAED,6FAA6F;IAC7F,6FAA6F;IAC7F,oFAAoF;IACpF,IAAI,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,KAAK,YAAY;QAAE,OAAO,MAAM,CAAC,MAAM,CAAC;IAEzE,OAAO,GAAG,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;AAClE,CAAC;AAED;;;;;GAKG;AACH,SAAS,eAAe,CAAC,GAAY;IACnC,MAAM,GAAG,GAAG,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,IAAI,GAAG,KAAK,SAAS,IAAI,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;QACpE,MAAM,IAAI,WAAW,CACnB,GAAG,cAAc,IAAI,GAAG,gEAAgE;YACtF,0FAA0F,cAAc,GAAG,CAC9G,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,YAA6B,EAAE,EAAE,MAAe,OAAO,CAAC,GAAG;IAC7F,eAAe,CAAC,GAAG,CAAC,CAAC;IAErB,MAAM,MAAM,GAAG,CAAC,SAAS,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;IACvD,MAAM,KAAK,GAAG,CAAC,SAAS,CAAC,KAAK,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;IAE1D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,WAAW,CACnB,+CAA+C,SAAS,2BAA2B;YACjF,yGAAyG,CAC5G,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,GAAG,EAAE,YAAY,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;AACpG,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Parses `.env` contents into pairs, ignoring blanks, comments, and malformed lines. */
|
|
2
|
+
export declare function parseEnvFile(contents: string): Record<string, string>;
|
|
3
|
+
/**
|
|
4
|
+
* Loads a `.env` file into `env`, leaving any variable that is already set untouched — the real
|
|
5
|
+
* environment and explicit flags outrank the file. A path the user named explicitly but that
|
|
6
|
+
* cannot be read is a `ConfigError`: silently continuing would leave the caller (often an agent)
|
|
7
|
+
* guessing why its credentials vanished.
|
|
8
|
+
*
|
|
9
|
+
* Only GitLab connection variables are applied; every other entry in the file is ignored.
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadEnvFile(path: string, env?: NodeJS.ProcessEnv): void;
|
package/lib/env-file.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
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 { readFileSync } from 'node:fs';
|
|
17
|
+
import process from 'node:process';
|
|
18
|
+
import { ConfigError } from './errors.js';
|
|
19
|
+
/**
|
|
20
|
+
* The only variables a `.env` file may set. Without this, a file the user did not write — one
|
|
21
|
+
* from a repo, a shared drive, or generated by an agent — could set `NODE_TLS_REJECT_UNAUTHORIZED`
|
|
22
|
+
* and turn off the certificate verification this CLI otherwise guarantees, handing the token to
|
|
23
|
+
* whoever answers. `GITLAB_SSL_VERIFY` is deliberately included so that setting it still produces the
|
|
24
|
+
* explanatory ConfigError rather than silence.
|
|
25
|
+
*/
|
|
26
|
+
const APPLICABLE_KEYS = new Set([
|
|
27
|
+
// Safe to allow: the real environment still wins, so a file can turn the guard on but never
|
|
28
|
+
// off. A read-only credential file carrying this is the intended arrangement.
|
|
29
|
+
'GITLAB_READ_ONLY',
|
|
30
|
+
'GITLAB_URL',
|
|
31
|
+
'GITLAB_TOKEN',
|
|
32
|
+
'GITLAB_SSL_VERIFY',
|
|
33
|
+
]);
|
|
34
|
+
/** Matches `KEY=value`, tolerating a leading `export` and surrounding whitespace. */
|
|
35
|
+
const ASSIGNMENT = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
|
|
36
|
+
/**
|
|
37
|
+
* Strips one layer of matching quotes. Only double quotes get escape processing, matching how
|
|
38
|
+
* shells treat them — `\n` in a single-quoted value is two literal characters.
|
|
39
|
+
*/
|
|
40
|
+
function unquote(raw) {
|
|
41
|
+
const value = raw.trim();
|
|
42
|
+
const quote = value.startsWith('"') ? '"' : value.startsWith("'") ? "'" : undefined;
|
|
43
|
+
if (quote !== undefined) {
|
|
44
|
+
// Anything after the closing quote is a comment, so a quoted value keeps its content even
|
|
45
|
+
// when the line ends in `# note` — otherwise the quotes end up inside the credential.
|
|
46
|
+
const closingAt = value.indexOf(quote, 1);
|
|
47
|
+
if (closingAt !== -1) {
|
|
48
|
+
const inner = value.slice(1, closingAt);
|
|
49
|
+
// Only double quotes carry escapes, matching how a shell treats them.
|
|
50
|
+
return quote === '"' ? inner.replaceAll('\\n', '\n').replaceAll('\\"', '"') : inner;
|
|
51
|
+
}
|
|
52
|
+
// Unterminated quote: fall through and treat the rest as a plain value.
|
|
53
|
+
}
|
|
54
|
+
// Unquoted values stop at an inline comment: `TOKEN=abc # note` is the token `abc`.
|
|
55
|
+
const commentAt = value.search(/\s#/);
|
|
56
|
+
return (commentAt === -1 ? value : value.slice(0, commentAt)).trim();
|
|
57
|
+
}
|
|
58
|
+
/** Parses `.env` contents into pairs, ignoring blanks, comments, and malformed lines. */
|
|
59
|
+
export function parseEnvFile(contents) {
|
|
60
|
+
const parsed = {};
|
|
61
|
+
// An editor-written .env can start with a BOM, which would otherwise become part of the key.
|
|
62
|
+
for (const line of contents.replace(/^\uFEFF/, '').split(/\r?\n/)) {
|
|
63
|
+
if (line.trim() === '' || line.trimStart().startsWith('#'))
|
|
64
|
+
continue;
|
|
65
|
+
const match = ASSIGNMENT.exec(line);
|
|
66
|
+
if (match?.[1] !== undefined)
|
|
67
|
+
parsed[match[1]] = unquote(match[2] ?? '');
|
|
68
|
+
}
|
|
69
|
+
return parsed;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Loads a `.env` file into `env`, leaving any variable that is already set untouched — the real
|
|
73
|
+
* environment and explicit flags outrank the file. A path the user named explicitly but that
|
|
74
|
+
* cannot be read is a `ConfigError`: silently continuing would leave the caller (often an agent)
|
|
75
|
+
* guessing why its credentials vanished.
|
|
76
|
+
*
|
|
77
|
+
* Only GitLab connection variables are applied; every other entry in the file is ignored.
|
|
78
|
+
*/
|
|
79
|
+
export function loadEnvFile(path, env = process.env) {
|
|
80
|
+
let contents;
|
|
81
|
+
try {
|
|
82
|
+
contents = readFileSync(path, 'utf8');
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const reason = error.code === 'ENOENT' ? 'not found' : 'could not be read';
|
|
86
|
+
throw new ConfigError(`Env file ${path} ${reason}.`);
|
|
87
|
+
}
|
|
88
|
+
for (const [key, value] of Object.entries(parseEnvFile(contents))) {
|
|
89
|
+
if (!APPLICABLE_KEYS.has(key))
|
|
90
|
+
continue;
|
|
91
|
+
// A variable exported as the empty string carries no value, so it must not outrank the
|
|
92
|
+
// file the user explicitly named — otherwise `export GITLAB_TOKEN=` in a wrapper script
|
|
93
|
+
// silently discards the real token and the run fails as if nothing were configured.
|
|
94
|
+
if (env[key] === undefined || env[key] === '')
|
|
95
|
+
env[key] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=env-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-file.js","sourceRoot":"","sources":["../src/env-file.ts"],"names":[],"mappings":"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"}
|
package/lib/errors.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for every error this CLI raises deliberately. Commands surface these through
|
|
3
|
+
* oclif's `this.error(message, { exit })` so users get formatted output rather than a stack
|
|
4
|
+
* trace for expected failure modes.
|
|
5
|
+
*/
|
|
6
|
+
export declare class CliError extends Error {
|
|
7
|
+
readonly exitCode: number;
|
|
8
|
+
constructor(message: string, exitCode?: number);
|
|
9
|
+
}
|
|
10
|
+
/** Missing, malformed, or self-contradictory configuration. */
|
|
11
|
+
export declare class ConfigError extends CliError {
|
|
12
|
+
constructor(message: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The instance rejected our credentials. Carries the status because 401 and 403 call for
|
|
16
|
+
* different responses: bad credentials versus a valid account without the needed permission.
|
|
17
|
+
*/
|
|
18
|
+
export declare class AuthError extends CliError {
|
|
19
|
+
readonly status: number;
|
|
20
|
+
constructor(message: string, status: number);
|
|
21
|
+
}
|
|
22
|
+
/** The instance never answered: timeout, DNS miss, refused connection, or an untrusted certificate. */
|
|
23
|
+
export declare class NetworkError extends CliError {
|
|
24
|
+
constructor(message: string);
|
|
25
|
+
}
|
|
26
|
+
/** Any other non-2xx response, carrying the status and whatever body came back. */
|
|
27
|
+
export declare class HttpError extends CliError {
|
|
28
|
+
readonly status: number;
|
|
29
|
+
readonly body: unknown;
|
|
30
|
+
constructor(message: string, status: number, body?: unknown);
|
|
31
|
+
}
|