@kici-dev/compiler 0.1.3 → 0.1.5
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/dist/cli.js +5 -3
- package/dist/commands/login.js +4 -6
- package/dist/commands/status.js +1 -1
- package/dist/execution/executor.js +1 -1
- package/dist/llm-context/llms-full.txt +217 -23
- package/dist/llm-context/llms.txt +1 -0
- package/dist/local-executor/index.js +30 -13
- package/dist/local-executor/job-runner.d.ts +4 -2
- package/dist/local-executor/job-runner.js +1 -1
- package/dist/local-executor/materializer.d.ts +39 -0
- package/dist/local-executor/materializer.js +111 -0
- package/dist/local-executor/types.d.ts +4 -0
- package/dist/remote/oauth.d.ts +9 -8
- package/dist/remote/oauth.js +32 -23
- package/dist/remote/oidc-discovery.d.ts +39 -0
- package/dist/remote/oidc-discovery.js +72 -0
- package/dist/remote/uploader.d.ts +35 -0
- package/dist/remote/uploader.js +32 -7
- package/dist/templates/package-json.js +1 -1
- package/package.json +4 -5
- package/sbom.spdx.json +52 -47
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialize an isolated tmp checkout of the repo for `kici run local`.
|
|
3
|
+
*
|
|
4
|
+
* Reproduces the same workspace `kici run remote` reconstructs on the agent:
|
|
5
|
+
* a clone checked out at HEAD, with the local overlay (dirty + untracked,
|
|
6
|
+
* minus gitignored, with `.kiciignore` applied) copied on top and local
|
|
7
|
+
* deletions removed. Steps then execute against this copy so the developer's
|
|
8
|
+
* real working tree is never mutated.
|
|
9
|
+
*
|
|
10
|
+
* Secrets are NOT part of the overlay: gitignored files such as
|
|
11
|
+
* `.kici/.env.local` are excluded by the shared selection, so the tmp checkout
|
|
12
|
+
* never receives them — the run reads them from the original `.kici/`.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* A materialized isolated checkout.
|
|
16
|
+
*/
|
|
17
|
+
export interface MaterializedCheckout {
|
|
18
|
+
/** Absolute path to the tmp checkout directory */
|
|
19
|
+
path: string;
|
|
20
|
+
/** Remove the tmp checkout directory */
|
|
21
|
+
cleanup: () => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Options for {@link materializeCheckout}.
|
|
25
|
+
*/
|
|
26
|
+
export interface MaterializeOptions {
|
|
27
|
+
/** Base directory for the tmp checkout (default: os.tmpdir()) */
|
|
28
|
+
runDir?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Materialize the repo at `repoRoot` into an isolated tmp checkout.
|
|
32
|
+
*
|
|
33
|
+
* @param repoRoot - Path to the git repository root
|
|
34
|
+
* @param opts - Optional configuration (tmp base directory)
|
|
35
|
+
* @returns The tmp checkout path and a cleanup callback
|
|
36
|
+
* @throws If `repoRoot` is not inside a git work tree
|
|
37
|
+
*/
|
|
38
|
+
export declare function materializeCheckout(repoRoot: string, opts?: MaterializeOptions): Promise<MaterializedCheckout>;
|
|
39
|
+
//# sourceMappingURL=materializer.d.ts.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import "../chunk-gOLHoazu.js";
|
|
2
|
+
import { selectOverlayFiles } from "../remote/uploader.js";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
8
|
+
//#region src/local-executor/materializer.ts
|
|
9
|
+
/**
|
|
10
|
+
* Materialize an isolated tmp checkout of the repo for `kici run local`.
|
|
11
|
+
*
|
|
12
|
+
* Reproduces the same workspace `kici run remote` reconstructs on the agent:
|
|
13
|
+
* a clone checked out at HEAD, with the local overlay (dirty + untracked,
|
|
14
|
+
* minus gitignored, with `.kiciignore` applied) copied on top and local
|
|
15
|
+
* deletions removed. Steps then execute against this copy so the developer's
|
|
16
|
+
* real working tree is never mutated.
|
|
17
|
+
*
|
|
18
|
+
* Secrets are NOT part of the overlay: gitignored files such as
|
|
19
|
+
* `.kici/.env.local` are excluded by the shared selection, so the tmp checkout
|
|
20
|
+
* never receives them — the run reads them from the original `.kici/`.
|
|
21
|
+
*/
|
|
22
|
+
/** Max number of overlay files copied concurrently. */
|
|
23
|
+
const COPY_BATCH_SIZE = 32;
|
|
24
|
+
/**
|
|
25
|
+
* Materialize the repo at `repoRoot` into an isolated tmp checkout.
|
|
26
|
+
*
|
|
27
|
+
* @param repoRoot - Path to the git repository root
|
|
28
|
+
* @param opts - Optional configuration (tmp base directory)
|
|
29
|
+
* @returns The tmp checkout path and a cleanup callback
|
|
30
|
+
* @throws If `repoRoot` is not inside a git work tree
|
|
31
|
+
*/
|
|
32
|
+
async function materializeCheckout(repoRoot, opts) {
|
|
33
|
+
requireGitRepo(repoRoot);
|
|
34
|
+
const base = opts?.runDir ?? os.tmpdir();
|
|
35
|
+
await fs.mkdir(base, { recursive: true });
|
|
36
|
+
const tmpDir = path.join(base, `kici-run-${randomBytes(3).toString("hex")}`);
|
|
37
|
+
const { sha, existingFiles, deletedFiles } = await selectOverlayFiles(repoRoot);
|
|
38
|
+
execSync(`git clone --no-hardlinks --quiet ${shellQuote(repoRoot)} ${shellQuote(tmpDir)}`, { stdio: "ignore" });
|
|
39
|
+
execSync(`git checkout --quiet ${sha}`, {
|
|
40
|
+
cwd: tmpDir,
|
|
41
|
+
stdio: "ignore"
|
|
42
|
+
});
|
|
43
|
+
await applyOverlay(repoRoot, tmpDir, existingFiles, deletedFiles);
|
|
44
|
+
return {
|
|
45
|
+
path: tmpDir,
|
|
46
|
+
cleanup: async () => {
|
|
47
|
+
await fs.rm(tmpDir, {
|
|
48
|
+
recursive: true,
|
|
49
|
+
force: true
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Throw an actionable error if `repoRoot` is not a git work tree.
|
|
56
|
+
*/
|
|
57
|
+
function requireGitRepo(repoRoot) {
|
|
58
|
+
try {
|
|
59
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
60
|
+
cwd: repoRoot,
|
|
61
|
+
stdio: "ignore"
|
|
62
|
+
});
|
|
63
|
+
} catch {
|
|
64
|
+
throw new Error(`kici run local needs a git repository to build an isolated checkout, but "${repoRoot}" is not inside a git work tree. Initialize a repo, or re-run with --in-place to execute against the working directory directly.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Copy the overlay files onto the clone and remove local deletions.
|
|
69
|
+
*/
|
|
70
|
+
async function applyOverlay(repoRoot, tmpDir, existingFiles, deletedFiles) {
|
|
71
|
+
for (let i = 0; i < existingFiles.length; i += COPY_BATCH_SIZE) {
|
|
72
|
+
const batch = existingFiles.slice(i, i + COPY_BATCH_SIZE);
|
|
73
|
+
await Promise.all(batch.map((file) => copyOverlayFile(repoRoot, tmpDir, file)));
|
|
74
|
+
}
|
|
75
|
+
for (let i = 0; i < deletedFiles.length; i += COPY_BATCH_SIZE) {
|
|
76
|
+
const batch = deletedFiles.slice(i, i + COPY_BATCH_SIZE);
|
|
77
|
+
await Promise.all(batch.map((file) => fs.rm(path.join(tmpDir, file), { force: true })));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Copy a single overlay file, preserving its mode (e.g. the exec bit).
|
|
82
|
+
*
|
|
83
|
+
* Symlinks are recreated as links rather than dereferenced — the same shape the
|
|
84
|
+
* remote path's tarball preserves. Following them would copy the link target's
|
|
85
|
+
* content (and a directory symlink such as `node_modules/@scope/pkg` would
|
|
86
|
+
* throw `EISDIR`), losing the link identity the workspace relies on.
|
|
87
|
+
*/
|
|
88
|
+
async function copyOverlayFile(repoRoot, tmpDir, file) {
|
|
89
|
+
const src = path.join(repoRoot, file);
|
|
90
|
+
const dest = path.join(tmpDir, file);
|
|
91
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
92
|
+
const srcStat = await fs.lstat(src);
|
|
93
|
+
if (srcStat.isSymbolicLink()) {
|
|
94
|
+
const target = await fs.readlink(src);
|
|
95
|
+
await fs.rm(dest, { force: true });
|
|
96
|
+
await fs.symlink(target, dest);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await fs.copyFile(src, dest);
|
|
100
|
+
await fs.chmod(dest, srcStat.mode);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Minimal single-quote shell escaping for paths passed to git via execSync.
|
|
104
|
+
*/
|
|
105
|
+
function shellQuote(value) {
|
|
106
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
export { materializeCheckout };
|
|
110
|
+
|
|
111
|
+
//# sourceMappingURL=materializer.js.map
|
|
@@ -38,6 +38,10 @@ export interface RunLocalOptions {
|
|
|
38
38
|
debug?: boolean;
|
|
39
39
|
/** --kici-dir: path to .kici directory */
|
|
40
40
|
kiciDir?: string;
|
|
41
|
+
/** --in-place: run against the real working directory instead of an isolated tmp checkout */
|
|
42
|
+
inPlace?: boolean;
|
|
43
|
+
/** --keep: always retain the isolated tmp checkout (default: keep only on failure) */
|
|
44
|
+
keep?: boolean;
|
|
41
45
|
}
|
|
42
46
|
/**
|
|
43
47
|
* A job after matrix expansion with resolved values.
|
package/dist/remote/oauth.d.ts
CHANGED
|
@@ -4,8 +4,6 @@ interface PkceFlowOptions {
|
|
|
4
4
|
issuer: string;
|
|
5
5
|
/** OAuth client ID for the CLI application */
|
|
6
6
|
clientId: string;
|
|
7
|
-
/** OIDC project ID (adds audience scope for JWT access tokens) */
|
|
8
|
-
projectId?: string;
|
|
9
7
|
}
|
|
10
8
|
/** Options for the RFC 8628 device authorization flow. */
|
|
11
9
|
interface DeviceFlowOptions {
|
|
@@ -13,8 +11,6 @@ interface DeviceFlowOptions {
|
|
|
13
11
|
issuer: string;
|
|
14
12
|
/** OAuth client ID for the CLI application */
|
|
15
13
|
clientId: string;
|
|
16
|
-
/** OIDC project ID (adds audience scope for JWT access tokens) */
|
|
17
|
-
projectId?: string;
|
|
18
14
|
}
|
|
19
15
|
/** Options for exchanging an OIDC token for a PAT. */
|
|
20
16
|
interface ExchangeTokenOptions {
|
|
@@ -38,8 +34,11 @@ interface ExchangeTokenResult {
|
|
|
38
34
|
* PKCE authorization code flow with localhost callback.
|
|
39
35
|
*
|
|
40
36
|
* Spins up a temporary HTTP server on a random port, opens the browser
|
|
41
|
-
* to the
|
|
42
|
-
* authorization code, and exchanges it for tokens.
|
|
37
|
+
* to the IdP's authorization endpoint, captures the callback with the
|
|
38
|
+
* authorization code, and exchanges it for tokens. The authorization
|
|
39
|
+
* and token endpoints come from the IdP's OIDC discovery document
|
|
40
|
+
* (`/.well-known/openid-configuration`) so the same flow works against
|
|
41
|
+
* any spec-compliant IdP regardless of its URL conventions.
|
|
43
42
|
*
|
|
44
43
|
* @returns OIDC access token
|
|
45
44
|
*/
|
|
@@ -47,9 +46,11 @@ export declare function pkceFlow(opts: PkceFlowOptions): Promise<string>;
|
|
|
47
46
|
/**
|
|
48
47
|
* RFC 8628 device authorization flow.
|
|
49
48
|
*
|
|
50
|
-
* Requests a device code from
|
|
49
|
+
* Requests a device code from the IdP, displays the user code and
|
|
51
50
|
* verification URI, then polls the token endpoint until the user
|
|
52
|
-
* authorizes the device or the code expires.
|
|
51
|
+
* authorizes the device or the code expires. Endpoints come from the
|
|
52
|
+
* OIDC discovery document so the same flow works against any
|
|
53
|
+
* spec-compliant IdP.
|
|
53
54
|
*
|
|
54
55
|
* @returns OIDC access token
|
|
55
56
|
*/
|
package/dist/remote/oauth.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "../chunk-gOLHoazu.js";
|
|
2
|
+
import { discoverOidcEndpoints } from "./oidc-discovery.js";
|
|
2
3
|
import pc from "picocolors";
|
|
3
4
|
import { exec } from "node:child_process";
|
|
4
5
|
import { toErrorMessage } from "@kici-dev/shared";
|
|
@@ -80,15 +81,16 @@ const SUCCESS_HTML = `<!DOCTYPE html>
|
|
|
80
81
|
* PKCE authorization code flow with localhost callback.
|
|
81
82
|
*
|
|
82
83
|
* Spins up a temporary HTTP server on a random port, opens the browser
|
|
83
|
-
* to the
|
|
84
|
-
* authorization code, and exchanges it for tokens.
|
|
84
|
+
* to the IdP's authorization endpoint, captures the callback with the
|
|
85
|
+
* authorization code, and exchanges it for tokens. The authorization
|
|
86
|
+
* and token endpoints come from the IdP's OIDC discovery document
|
|
87
|
+
* (`/.well-known/openid-configuration`) so the same flow works against
|
|
88
|
+
* any spec-compliant IdP regardless of its URL conventions.
|
|
85
89
|
*
|
|
86
90
|
* @returns OIDC access token
|
|
87
91
|
*/
|
|
88
92
|
async function pkceFlow(opts) {
|
|
89
|
-
const { issuer, clientId
|
|
90
|
-
const { verifier, challenge } = generatePkceChallenge();
|
|
91
|
-
const state = randomBytes(16).toString("hex");
|
|
93
|
+
const { issuer, clientId } = opts;
|
|
92
94
|
let listenPort = 0;
|
|
93
95
|
const callbackPortEnv = process.env.KICI_CALLBACK_PORT;
|
|
94
96
|
if (callbackPortEnv) {
|
|
@@ -96,6 +98,9 @@ async function pkceFlow(opts) {
|
|
|
96
98
|
if (isNaN(parsed) || parsed < 0) throw new Error(`KICI_CALLBACK_PORT must be a valid non-negative integer, got: "${callbackPortEnv}"`);
|
|
97
99
|
listenPort = parsed;
|
|
98
100
|
}
|
|
101
|
+
const endpoints = await discoverOidcEndpoints(issuer);
|
|
102
|
+
const { verifier, challenge } = generatePkceChallenge();
|
|
103
|
+
const state = randomBytes(16).toString("hex");
|
|
99
104
|
const TIMEOUT_MS = 300 * 1e3;
|
|
100
105
|
return new Promise((resolve, reject) => {
|
|
101
106
|
let timer;
|
|
@@ -135,7 +140,7 @@ async function pkceFlow(opts) {
|
|
|
135
140
|
res.end(SUCCESS_HTML);
|
|
136
141
|
const port = server.address().port;
|
|
137
142
|
try {
|
|
138
|
-
const tokenRes = await fetchOrThrow(
|
|
143
|
+
const tokenRes = await fetchOrThrow(endpoints.token_endpoint, {
|
|
139
144
|
method: "POST",
|
|
140
145
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
141
146
|
body: new URLSearchParams({
|
|
@@ -145,7 +150,7 @@ async function pkceFlow(opts) {
|
|
|
145
150
|
redirect_uri: `http://127.0.0.1:${port}/callback`,
|
|
146
151
|
code_verifier: verifier
|
|
147
152
|
})
|
|
148
|
-
}, "
|
|
153
|
+
}, "IdP");
|
|
149
154
|
if (!tokenRes.ok) {
|
|
150
155
|
const errorBody = await tokenRes.text();
|
|
151
156
|
cleanup();
|
|
@@ -167,18 +172,16 @@ async function pkceFlow(opts) {
|
|
|
167
172
|
server.listen(listenPort, "127.0.0.1", () => {
|
|
168
173
|
const actualPort = server.address().port;
|
|
169
174
|
const redirectUri = `http://127.0.0.1:${actualPort}/callback`;
|
|
170
|
-
const authUrl = new URL(
|
|
175
|
+
const authUrl = new URL(endpoints.authorization_endpoint);
|
|
171
176
|
authUrl.searchParams.set("client_id", clientId);
|
|
172
177
|
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
173
178
|
authUrl.searchParams.set("response_type", "code");
|
|
174
|
-
|
|
179
|
+
authUrl.searchParams.set("scope", [
|
|
175
180
|
"openid",
|
|
176
181
|
"profile",
|
|
177
182
|
"email",
|
|
178
183
|
"offline_access"
|
|
179
|
-
];
|
|
180
|
-
if (projectId) scopes.push(`urn:zitadel:iam:org:project:id:${projectId}:aud`);
|
|
181
|
-
authUrl.searchParams.set("scope", scopes.join(" "));
|
|
184
|
+
].join(" "));
|
|
182
185
|
authUrl.searchParams.set("code_challenge", challenge);
|
|
183
186
|
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
184
187
|
authUrl.searchParams.set("state", state);
|
|
@@ -209,29 +212,34 @@ function sleep(ms) {
|
|
|
209
212
|
/**
|
|
210
213
|
* RFC 8628 device authorization flow.
|
|
211
214
|
*
|
|
212
|
-
* Requests a device code from
|
|
215
|
+
* Requests a device code from the IdP, displays the user code and
|
|
213
216
|
* verification URI, then polls the token endpoint until the user
|
|
214
|
-
* authorizes the device or the code expires.
|
|
217
|
+
* authorizes the device or the code expires. Endpoints come from the
|
|
218
|
+
* OIDC discovery document so the same flow works against any
|
|
219
|
+
* spec-compliant IdP.
|
|
215
220
|
*
|
|
216
221
|
* @returns OIDC access token
|
|
217
222
|
*/
|
|
218
223
|
async function deviceFlow(opts) {
|
|
219
|
-
const { issuer, clientId
|
|
224
|
+
const { issuer, clientId } = opts;
|
|
225
|
+
const endpoints = await discoverOidcEndpoints(issuer);
|
|
220
226
|
const scopes = [
|
|
221
227
|
"openid",
|
|
222
228
|
"profile",
|
|
223
229
|
"email",
|
|
224
230
|
"offline_access"
|
|
225
231
|
];
|
|
226
|
-
|
|
227
|
-
const deviceAuthRes = await fetchOrThrow(
|
|
232
|
+
const { verifier, challenge } = generatePkceChallenge();
|
|
233
|
+
const deviceAuthRes = await fetchOrThrow(endpoints.device_authorization_endpoint, {
|
|
228
234
|
method: "POST",
|
|
229
235
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
230
236
|
body: new URLSearchParams({
|
|
231
237
|
client_id: clientId,
|
|
232
|
-
scope: scopes.join(" ")
|
|
238
|
+
scope: scopes.join(" "),
|
|
239
|
+
code_challenge: challenge,
|
|
240
|
+
code_challenge_method: "S256"
|
|
233
241
|
})
|
|
234
|
-
}, "
|
|
242
|
+
}, "IdP");
|
|
235
243
|
if (!deviceAuthRes.ok) {
|
|
236
244
|
const errorBody = await deviceAuthRes.text();
|
|
237
245
|
throw new Error(`Device authorization request failed: ${errorBody}`);
|
|
@@ -241,7 +249,7 @@ async function deviceFlow(opts) {
|
|
|
241
249
|
console.log(pc.bold(" Device authorization"));
|
|
242
250
|
if (deviceAuth.verification_uri_complete) {
|
|
243
251
|
console.log(` Open ${pc.cyan(deviceAuth.verification_uri_complete)}`);
|
|
244
|
-
console.log(pc.gray(" The code is pre-filled; approve on the
|
|
252
|
+
console.log(pc.gray(" The code is pre-filled; approve on the IdP screen."));
|
|
245
253
|
console.log(pc.gray(` Enter code: ${pc.bold(pc.yellow(deviceAuth.user_code))} (if prompted)`));
|
|
246
254
|
} else {
|
|
247
255
|
console.log(` Open ${pc.cyan(deviceAuth.verification_uri)}`);
|
|
@@ -252,15 +260,16 @@ async function deviceFlow(opts) {
|
|
|
252
260
|
let intervalMs = (deviceAuth.interval ?? 5) * 1e3;
|
|
253
261
|
while (true) {
|
|
254
262
|
await sleep(intervalMs);
|
|
255
|
-
const tokenRes = await fetchOrThrow(
|
|
263
|
+
const tokenRes = await fetchOrThrow(endpoints.token_endpoint, {
|
|
256
264
|
method: "POST",
|
|
257
265
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
258
266
|
body: new URLSearchParams({
|
|
259
267
|
client_id: clientId,
|
|
260
268
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
261
|
-
device_code: deviceAuth.device_code
|
|
269
|
+
device_code: deviceAuth.device_code,
|
|
270
|
+
code_verifier: verifier
|
|
262
271
|
})
|
|
263
|
-
}, "
|
|
272
|
+
}, "IdP");
|
|
264
273
|
if (tokenRes.ok) return (await tokenRes.json()).access_token;
|
|
265
274
|
const errorData = await tokenRes.json();
|
|
266
275
|
switch (errorData.error) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Endpoints the CLI's OAuth flows need from an OIDC IdP. A subset of the
|
|
3
|
+
* full OIDC Provider Metadata schema (RFC 8414 / OpenID Connect Discovery
|
|
4
|
+
* 1.0) — we only consume what `pkceFlow` / `deviceFlow` actually call.
|
|
5
|
+
*/
|
|
6
|
+
export interface OidcDiscoveryEndpoints {
|
|
7
|
+
/** Issuer claim — used for sanity-checking the discovery document. */
|
|
8
|
+
issuer: string;
|
|
9
|
+
/** RFC 6749 authorization endpoint (PKCE). */
|
|
10
|
+
authorization_endpoint: string;
|
|
11
|
+
/** RFC 6749 token endpoint (PKCE code exchange + device-flow polling). */
|
|
12
|
+
token_endpoint: string;
|
|
13
|
+
/** RFC 8628 device authorization endpoint. */
|
|
14
|
+
device_authorization_endpoint: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Test-only escape hatch. Clears the per-process cache so a subsequent
|
|
18
|
+
* `discoverOidcEndpoints` call re-fetches the metadata document.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resetDiscoveryCache(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Fetch and parse the OIDC Provider Metadata document for an issuer URL,
|
|
23
|
+
* then return the endpoints the CLI needs. Cached per-process so a `kici
|
|
24
|
+
* login` invocation makes at most one discovery round-trip.
|
|
25
|
+
*
|
|
26
|
+
* The issuer is treated as a base URL — trailing slashes are stripped and
|
|
27
|
+
* `/.well-known/openid-configuration` is appended. This works for the two
|
|
28
|
+
* IdP shapes the CLI authenticates against:
|
|
29
|
+
*
|
|
30
|
+
* - Keycloak: issuer = `https://auth.example.com/realms/<name>` (path-segment).
|
|
31
|
+
* - Keycloak: issuer = `https://auth.example.com/realms/<realm>`
|
|
32
|
+
* (realm-scoped — discovery lives at `<issuer>/.well-known/...`).
|
|
33
|
+
*
|
|
34
|
+
* Throws with an actionable message if discovery is unreachable (DNS,
|
|
35
|
+
* connection refused, TLS handshake), returns a non-2xx status, fails to
|
|
36
|
+
* parse as JSON, or omits any of the four required fields.
|
|
37
|
+
*/
|
|
38
|
+
export declare function discoverOidcEndpoints(issuer: string): Promise<OidcDiscoveryEndpoints>;
|
|
39
|
+
//# sourceMappingURL=oidc-discovery.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import "../chunk-gOLHoazu.js";
|
|
2
|
+
import { toErrorMessage } from "@kici-dev/shared";
|
|
3
|
+
//#region src/remote/oidc-discovery.ts
|
|
4
|
+
const cache = /* @__PURE__ */ new Map();
|
|
5
|
+
/**
|
|
6
|
+
* Test-only escape hatch. Clears the per-process cache so a subsequent
|
|
7
|
+
* `discoverOidcEndpoints` call re-fetches the metadata document.
|
|
8
|
+
*/
|
|
9
|
+
function resetDiscoveryCache() {
|
|
10
|
+
cache.clear();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Fetch and parse the OIDC Provider Metadata document for an issuer URL,
|
|
14
|
+
* then return the endpoints the CLI needs. Cached per-process so a `kici
|
|
15
|
+
* login` invocation makes at most one discovery round-trip.
|
|
16
|
+
*
|
|
17
|
+
* The issuer is treated as a base URL — trailing slashes are stripped and
|
|
18
|
+
* `/.well-known/openid-configuration` is appended. This works for the two
|
|
19
|
+
* IdP shapes the CLI authenticates against:
|
|
20
|
+
*
|
|
21
|
+
* - Keycloak: issuer = `https://auth.example.com/realms/<name>` (path-segment).
|
|
22
|
+
* - Keycloak: issuer = `https://auth.example.com/realms/<realm>`
|
|
23
|
+
* (realm-scoped — discovery lives at `<issuer>/.well-known/...`).
|
|
24
|
+
*
|
|
25
|
+
* Throws with an actionable message if discovery is unreachable (DNS,
|
|
26
|
+
* connection refused, TLS handshake), returns a non-2xx status, fails to
|
|
27
|
+
* parse as JSON, or omits any of the four required fields.
|
|
28
|
+
*/
|
|
29
|
+
async function discoverOidcEndpoints(issuer) {
|
|
30
|
+
const base = issuer.trim().replace(/\/+$/, "");
|
|
31
|
+
if (!base) throw new Error("[oidc-discovery] issuer is empty");
|
|
32
|
+
const cached = cache.get(base);
|
|
33
|
+
if (cached) return cached;
|
|
34
|
+
const url = `${base}/.well-known/openid-configuration`;
|
|
35
|
+
let res;
|
|
36
|
+
try {
|
|
37
|
+
res = await fetch(url, { method: "GET" });
|
|
38
|
+
} catch (err) {
|
|
39
|
+
throw new Error(`Could not reach IdP discovery at ${url} (${toErrorMessage(err)}). Check KICI_OIDC_ISSUER.`);
|
|
40
|
+
}
|
|
41
|
+
if (!res.ok) throw new Error(`IdP discovery at ${url} returned HTTP ${res.status} ${res.statusText}`);
|
|
42
|
+
let body;
|
|
43
|
+
try {
|
|
44
|
+
body = await res.json();
|
|
45
|
+
} catch (err) {
|
|
46
|
+
throw new Error(`IdP discovery at ${url} returned non-JSON body: ${toErrorMessage(err)}`);
|
|
47
|
+
}
|
|
48
|
+
const parsed = parseEndpoints(body, url);
|
|
49
|
+
cache.set(base, parsed);
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
function parseEndpoints(body, url) {
|
|
53
|
+
if (!body || typeof body !== "object") throw new Error(`IdP discovery at ${url} returned a non-object payload.`);
|
|
54
|
+
const obj = body;
|
|
55
|
+
const missing = [
|
|
56
|
+
"issuer",
|
|
57
|
+
"authorization_endpoint",
|
|
58
|
+
"token_endpoint",
|
|
59
|
+
"device_authorization_endpoint"
|
|
60
|
+
].filter((k) => typeof obj[k] !== "string" || !obj[k]);
|
|
61
|
+
if (missing.length > 0) throw new Error(`IdP discovery at ${url} is missing required field(s): ${missing.join(", ")}. The OIDC IdP must advertise authorization_endpoint, token_endpoint, and device_authorization_endpoint via /.well-known/openid-configuration.`);
|
|
62
|
+
return {
|
|
63
|
+
issuer: String(obj.issuer),
|
|
64
|
+
authorization_endpoint: String(obj.authorization_endpoint),
|
|
65
|
+
token_endpoint: String(obj.token_endpoint),
|
|
66
|
+
device_authorization_endpoint: String(obj.device_authorization_endpoint)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { discoverOidcEndpoints, resetDiscoveryCache };
|
|
71
|
+
|
|
72
|
+
//# sourceMappingURL=oidc-discovery.js.map
|
|
@@ -51,6 +51,41 @@ interface UploadResult {
|
|
|
51
51
|
/** Encrypted tarball size in bytes */
|
|
52
52
|
encryptedSize: number;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Result of selecting which files form the overlay over a clone at HEAD.
|
|
56
|
+
*
|
|
57
|
+
* Both the remote uploader and the local materializer consume this so the two
|
|
58
|
+
* paths reconstruct the same workspace from the same selection logic.
|
|
59
|
+
*/
|
|
60
|
+
export interface OverlaySelection {
|
|
61
|
+
/** HEAD SHA the selection is based on */
|
|
62
|
+
sha: string;
|
|
63
|
+
/** Whether the repo has at least one git remote */
|
|
64
|
+
hasRemote: boolean;
|
|
65
|
+
/** Selected files that exist on disk (to copy onto the clone) */
|
|
66
|
+
existingFiles: string[];
|
|
67
|
+
/** Selected files missing on disk (to delete from the clone) */
|
|
68
|
+
deletedFiles: string[];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Select which files form the overlay over a clone checked out at HEAD.
|
|
72
|
+
*
|
|
73
|
+
* For repos with a remote: collects only dirty files (staged, unstaged, untracked).
|
|
74
|
+
* For repos without a remote: collects ALL tracked + untracked files.
|
|
75
|
+
*
|
|
76
|
+
* The selected set is filtered by `.kiciignore` (picomatch) and partitioned
|
|
77
|
+
* into files that still exist on disk (to copy onto the clone) and files that
|
|
78
|
+
* are missing (to delete from the clone). Gitignored files — including secret
|
|
79
|
+
* files like `.kici/.env.local` — are excluded by `--exclude-standard` and
|
|
80
|
+
* never appear in the selection.
|
|
81
|
+
*
|
|
82
|
+
* @param repoRoot - Path to the git repository root
|
|
83
|
+
* @param options - Optional configuration
|
|
84
|
+
* @returns HEAD SHA, remote flag, and the existing/deleted file partition
|
|
85
|
+
*/
|
|
86
|
+
export declare function selectOverlayFiles(repoRoot: string, options?: {
|
|
87
|
+
kiciIgnorePath?: string;
|
|
88
|
+
}): Promise<OverlaySelection>;
|
|
54
89
|
/**
|
|
55
90
|
* Create an overlay tarball from a git repo, including only changed files.
|
|
56
91
|
*
|
package/dist/remote/uploader.js
CHANGED
|
@@ -45,19 +45,22 @@ async function loadKiciIgnore(kiciIgnorePath) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
48
|
+
* Select which files form the overlay over a clone checked out at HEAD.
|
|
49
49
|
*
|
|
50
50
|
* For repos with a remote: collects only dirty files (staged, unstaged, untracked).
|
|
51
|
-
* For repos without a remote: collects ALL tracked + untracked files
|
|
51
|
+
* For repos without a remote: collects ALL tracked + untracked files.
|
|
52
52
|
*
|
|
53
|
-
* The
|
|
54
|
-
*
|
|
53
|
+
* The selected set is filtered by `.kiciignore` (picomatch) and partitioned
|
|
54
|
+
* into files that still exist on disk (to copy onto the clone) and files that
|
|
55
|
+
* are missing (to delete from the clone). Gitignored files — including secret
|
|
56
|
+
* files like `.kici/.env.local` — are excluded by `--exclude-standard` and
|
|
57
|
+
* never appear in the selection.
|
|
55
58
|
*
|
|
56
59
|
* @param repoRoot - Path to the git repository root
|
|
57
60
|
* @param options - Optional configuration
|
|
58
|
-
* @returns
|
|
61
|
+
* @returns HEAD SHA, remote flag, and the existing/deleted file partition
|
|
59
62
|
*/
|
|
60
|
-
async function
|
|
63
|
+
async function selectOverlayFiles(repoRoot, options) {
|
|
61
64
|
const sha = execSync("git rev-parse HEAD", {
|
|
62
65
|
cwd: repoRoot,
|
|
63
66
|
encoding: "utf-8"
|
|
@@ -91,6 +94,28 @@ async function createOverlayTarball(repoRoot, options) {
|
|
|
91
94
|
deletedFiles.push(file);
|
|
92
95
|
}
|
|
93
96
|
}));
|
|
97
|
+
return {
|
|
98
|
+
sha,
|
|
99
|
+
hasRemote,
|
|
100
|
+
existingFiles,
|
|
101
|
+
deletedFiles
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Create an overlay tarball from a git repo, including only changed files.
|
|
106
|
+
*
|
|
107
|
+
* For repos with a remote: collects only dirty files (staged, unstaged, untracked).
|
|
108
|
+
* For repos without a remote: collects ALL tracked + untracked files (full tarball).
|
|
109
|
+
*
|
|
110
|
+
* The tarball includes a `manifest.json` with the HEAD SHA, deletions list,
|
|
111
|
+
* and SHA256 checksums for integrity verification.
|
|
112
|
+
*
|
|
113
|
+
* @param repoRoot - Path to the git repository root
|
|
114
|
+
* @param options - Optional configuration
|
|
115
|
+
* @returns Tarball path, upload summary, and overlay manifest
|
|
116
|
+
*/
|
|
117
|
+
async function createOverlayTarball(repoRoot, options) {
|
|
118
|
+
const { sha, hasRemote, existingFiles, deletedFiles } = await selectOverlayFiles(repoRoot, options);
|
|
94
119
|
const untrackedSet = new Set(gitLines("git ls-files --others --exclude-standard", repoRoot));
|
|
95
120
|
const newFiles = existingFiles.filter((f) => untrackedSet.has(f));
|
|
96
121
|
const modifiedFiles = existingFiles.filter((f) => !untrackedSet.has(f));
|
|
@@ -192,6 +217,6 @@ async function uploadTarball(opts) {
|
|
|
192
217
|
throw new Error(`Upload failed after ${MAX_RETRIES} attempts: ${lastError?.message}`);
|
|
193
218
|
}
|
|
194
219
|
//#endregion
|
|
195
|
-
export { createOverlayTarball, getSizeWarning, uploadTarball };
|
|
220
|
+
export { createOverlayTarball, getSizeWarning, selectOverlayFiles, uploadTarball };
|
|
196
221
|
|
|
197
222
|
//# sourceMappingURL=uploader.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kici-dev/compiler",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Compiler and CLI for KiCI workflows. Compiles `.kici/workflows/*.ts` to a `kici.lock.json` file consumed by the orchestrator and agents, and runs workflows locally or against a remote orchestrator.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kici",
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
"node": ">=24"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|
|
26
|
-
"registry": "https://registry.npmjs.org/",
|
|
27
26
|
"access": "public"
|
|
28
27
|
},
|
|
29
28
|
"type": "module",
|
|
@@ -59,11 +58,11 @@
|
|
|
59
58
|
"ws": "^8.20.0",
|
|
60
59
|
"yaml": "^2.8.3",
|
|
61
60
|
"zx": "^8.8.5",
|
|
62
|
-
"@kici-dev/engine": "0.1.
|
|
63
|
-
"@kici-dev/shared": "0.1.
|
|
61
|
+
"@kici-dev/engine": "0.1.5",
|
|
62
|
+
"@kici-dev/shared": "0.1.5"
|
|
64
63
|
},
|
|
65
64
|
"peerDependencies": {
|
|
66
|
-
"@kici-dev/sdk": "0.1.
|
|
65
|
+
"@kici-dev/sdk": "0.1.5"
|
|
67
66
|
},
|
|
68
67
|
"devDependencies": {
|
|
69
68
|
"@types/proper-lockfile": "^4.1.4"
|