@fjall/util 0.96.0 → 0.99.3
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/.minified +1 -1
- package/dist/Config.d.ts +1 -1
- package/dist/appPath.d.ts +29 -0
- package/dist/appPath.js +1 -0
- package/dist/constructMap.d.ts +9 -13
- package/dist/constructMap.js +1 -1
- package/dist/deriveContentHashTag.d.ts +34 -0
- package/dist/deriveContentHashTag.js +1 -0
- package/dist/docker/DockerCli.build.d.ts +42 -0
- package/dist/docker/DockerCli.build.js +1 -0
- package/dist/docker/DockerCli.d.ts +135 -0
- package/dist/docker/DockerCli.daemon.d.ts +24 -0
- package/dist/docker/DockerCli.daemon.js +1 -0
- package/dist/docker/DockerCli.js +1 -0
- package/dist/docker/DockerCli.registry.d.ts +19 -0
- package/dist/docker/DockerCli.registry.js +3 -0
- package/dist/docker/abortHelpers.d.ts +18 -0
- package/dist/docker/abortHelpers.js +1 -0
- package/dist/docker/buildxArgvBuilder.d.ts +15 -0
- package/dist/docker/buildxArgvBuilder.js +1 -0
- package/dist/docker/dockerCliConstants.d.ts +15 -0
- package/dist/docker/dockerCliConstants.js +1 -0
- package/dist/docker/dockerCliSchemas.d.ts +88 -0
- package/dist/docker/dockerCliSchemas.js +1 -0
- package/dist/docker/index.d.ts +10 -0
- package/dist/docker/index.js +1 -0
- package/dist/docker/metadataFileParser.d.ts +17 -0
- package/dist/docker/metadataFileParser.js +1 -0
- package/dist/docker/projectBuildxResult.d.ts +35 -0
- package/dist/docker/projectBuildxResult.js +1 -0
- package/dist/docker/rawjsonParser.d.ts +56 -0
- package/dist/docker/rawjsonParser.js +1 -0
- package/dist/docker/rawjsonToVertexEvent.d.ts +64 -0
- package/dist/docker/rawjsonToVertexEvent.js +1 -0
- package/dist/docker/result.d.ts +34 -0
- package/dist/docker/result.js +1 -0
- package/dist/errorUtils.d.ts +14 -4
- package/dist/findInfrastructurePaths.d.ts +62 -0
- package/dist/findInfrastructurePaths.js +1 -0
- package/dist/findRepoRoot.d.ts +12 -0
- package/dist/findRepoRoot.js +1 -0
- package/dist/fsHelpers.d.ts +15 -0
- package/dist/fsHelpers.js +1 -1
- package/dist/fsScan.d.ts +15 -0
- package/dist/fsScan.js +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1 -1
- package/dist/inferContainerFromCandidates.d.ts +23 -0
- package/dist/inferContainerFromCandidates.js +1 -0
- package/dist/manifest/index.d.ts +10 -0
- package/dist/manifest/index.js +1 -0
- package/dist/manifest/io.d.ts +60 -0
- package/dist/manifest/io.js +1 -0
- package/dist/manifest/schemas.d.ts +163 -0
- package/dist/manifest/schemas.js +1 -0
- package/dist/mcpProtocol/index.d.ts +362 -0
- package/dist/mcpProtocol/index.js +1 -0
- package/dist/migration/clickhouseSqlUsers.d.ts +72 -0
- package/dist/migration/clickhouseSqlUsers.js +1 -0
- package/dist/migration/constants.d.ts +34 -0
- package/dist/migration/constants.js +1 -0
- package/dist/migration/index.d.ts +3 -0
- package/dist/migration/index.js +1 -0
- package/dist/migration/pickLatestPrismaMigration.d.ts +10 -0
- package/dist/migration/pickLatestPrismaMigration.js +1 -0
- package/dist/reservedAppNames.d.ts +30 -0
- package/dist/reservedAppNames.js +1 -0
- package/dist/scanLocalRepository.d.ts +24 -0
- package/dist/scanLocalRepository.js +1 -0
- package/dist/scanTypes.d.ts +17 -0
- package/dist/scanTypes.js +0 -0
- package/dist/securityHelpers.d.ts +11 -1
- package/dist/securityHelpers.js +1 -1
- package/dist/tokenScopes.d.ts +11 -0
- package/dist/tokenScopes.js +1 -0
- package/package.json +43 -9
package/dist/.minified
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
53 files minified at 2026-05-22T22:51:39.799Z
|
package/dist/Config.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export declare const RootConfigSchema: z.ZodObject<{
|
|
|
43
43
|
account: z.ZodOptional<z.ZodString>;
|
|
44
44
|
}, z.core.$strict>>>;
|
|
45
45
|
}, z.core.$strict>;
|
|
46
|
-
type RootConfig = z.infer<typeof RootConfigSchema>;
|
|
46
|
+
export type RootConfig = z.infer<typeof RootConfigSchema>;
|
|
47
47
|
/**
|
|
48
48
|
* Config class for loading and saving the root fjall-config.json.
|
|
49
49
|
* Single config file at fjall/fjall-config.json (or fjall-config.json at cwd).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-layout conventions shared by CLI, webapp, and worker code.
|
|
3
|
+
*
|
|
4
|
+
* The Fjall scaffolder writes application infrastructure under a `fjall/`
|
|
5
|
+
* boundary directory. The customer-chosen app name lives **inside** that
|
|
6
|
+
* boundary, not as a sibling of it. Two layouts are supported:
|
|
7
|
+
*
|
|
8
|
+
* - Default (no container): `fjall/<name>/infrastructure.ts`
|
|
9
|
+
* - With container: `<container>/fjall/<name>/infrastructure.ts`
|
|
10
|
+
*
|
|
11
|
+
* The `fjall/` segment is the codemod blast-radius boundary; the
|
|
12
|
+
* `--container` argument names the directory that sits between the repo
|
|
13
|
+
* root and the boundary. Two `fjall create app` calls with the same
|
|
14
|
+
* container value land as siblings inside one shared boundary (the T3
|
|
15
|
+
* multi-app shape), e.g.
|
|
16
|
+
* `webapp/fjall/api/infrastructure.ts`
|
|
17
|
+
* `webapp/fjall/worker/infrastructure.ts`
|
|
18
|
+
*
|
|
19
|
+
* Multiple producers (the register-application flow, the auto-link
|
|
20
|
+
* repository flow, the post-create linkage flow, the CLI `cd …` message
|
|
21
|
+
* strings) all derive this path from this helper — keeping it as inline
|
|
22
|
+
* template literals would invite silent CLI/webapp drift if the
|
|
23
|
+
* convention shifts.
|
|
24
|
+
*
|
|
25
|
+
* See `aiDocs/designs/2026-05-11-marker-as-boundary-not-identity.md` and
|
|
26
|
+
* `aiDocs/patterns/fjall-marker-convention-pattern.md` for the
|
|
27
|
+
* convention's full topology mapping.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildAppConfigPath(appName: string, container?: string): string;
|
package/dist/appPath.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function t(r,n){if(n!==void 0){if(n.trim()==="")throw new Error("buildAppConfigPath: container must be a non-empty string");return`${n}/fjall/${r}`}return`fjall/${r}`}export{t as buildAppConfigPath};
|
package/dist/constructMap.d.ts
CHANGED
|
@@ -8,20 +8,16 @@
|
|
|
8
8
|
* When a new CDK construct is added, add one entry here.
|
|
9
9
|
* When a new AWS resource type is added, nothing changes.
|
|
10
10
|
*/
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Manifest filename + schema version + ResourceMapEntry shape are re-exported
|
|
13
|
+
* from `./manifest/schemas` (the canonical home for manifest types — that
|
|
14
|
+
* module is pure and is safe to import from environments that must avoid `fs`,
|
|
15
|
+
* e.g. the generator).
|
|
16
|
+
*/
|
|
17
|
+
export { FJALL_MANIFEST_FILENAME, MANIFEST_SCHEMA_VERSION } from "./manifest/schemas.js";
|
|
18
|
+
export type { ResourceMapEntry } from "./manifest/schemas.js";
|
|
19
|
+
import type { ResourceMapEntry } from "./manifest/schemas.js";
|
|
15
20
|
import type { ResourceCategory } from "./resourceCategorisation.js";
|
|
16
|
-
/** Entry in the resource map — maps a logical ID to its construct context. */
|
|
17
|
-
export interface ResourceMapEntry {
|
|
18
|
-
/** Full CDK construct path (e.g., "/Account/CloudTrail/managementEventsTrail/Resource") */
|
|
19
|
-
constructPath: string;
|
|
20
|
-
/** Topology group derived from the construct (e.g., "monitoring") */
|
|
21
|
-
group: string;
|
|
22
|
-
/** AWS resource type (e.g., "AWS::KMS::Key") */
|
|
23
|
-
resourceType: string;
|
|
24
|
-
}
|
|
25
21
|
/**
|
|
26
22
|
* Account stack construct-to-group mapping.
|
|
27
23
|
* Keys are CDK construct IDs (first segment after stack name in construct path).
|
package/dist/constructMap.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import{FJALL_MANIFEST_FILENAME as _,MANIFEST_SCHEMA_VERSION as w}from"./manifest/schemas.js";import{readFileSync as p}from"fs";import{join as l}from"path";import{logger as g}from"./logger.js";import{categoriseResource as d,getFriendlyResourceType as y}from"./resourceCategorisation.js";const N=Object.freeze({CloudTrail:"monitoring",MonitoringRole:"monitoring",AuditRole:"security",OidcConnector:"security",EcrDefaultImage:"registry",EventBus:"events",DisasterRecovery:"backup"}),h=Object.freeze({Network:"network",Database:"database",Compute:"compute",Storage:"storage",Messaging:"events",Cdn:"dns"});function C(o,e){const t=o.split("/").filter(Boolean);if(t.length<2)return;const r=t[1];return e[r]}function T(o){const e=o.split("/").filter(Boolean);return e.length<2?o:(e.length>2&&e[e.length-1]==="Resource"?e[e.length-2]:e[e.length-1]).replace(/([a-z])([A-Z])/g,"$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g,"$1 $2")}function j(o,e){const t=new Map;let r;try{const n=p(l(o,"manifest.json"),"utf-8"),s=JSON.parse(n);if(typeof s!="object"||s===null)return t;r=s}catch(n){return g.debug("ConstructMap","Could not read CDK manifest",{error:n instanceof Error?n.message:String(n)}),t}if(!r.artifacts)return t;for(const n of Object.values(r.artifacts)){if(n.type!=="aws:cloudformation:stack"||!n.metadata)continue;const s=b(o,n.properties?.templateFile);for(const[a,c]of Object.entries(n.metadata))for(const u of c){if(u.type!=="aws:cdk:logicalId")continue;const i=u.data,f=s.get(i)??"",m=C(a,e)??d(f);t.set(i,{constructPath:a,group:m,resourceType:f})}}return t}function b(o,e){const t=new Map;if(!e)return t;try{const r=p(l(o,e),"utf-8"),n=JSON.parse(r);if(typeof n!="object"||n===null)return t;const s=n;if(s.Resources&&typeof s.Resources=="object")for(const[a,c]of Object.entries(s.Resources))typeof c=="object"&&c!==null&&c.Type&&t.set(a,c.Type)}catch(r){g.debug("ConstructMap","Could not read template file",{templateFile:e,error:r instanceof Error?r.message:String(r)})}return t}function A(o){const e={};for(const[t,r]of o)e[t]=r;return e}function E(o){const e=new Map;if(!o)return e;for(const[t,r]of Object.entries(o))e.set(t,r);return e}function v(o,e,t){const r=t?.get(o);return r?{group:r.group,constructPath:r.constructPath,displayName:T(r.constructPath)}:{displayName:y(e)}}export{N as ACCOUNT_CONSTRUCT_GROUPS,h as APP_CONSTRUCT_GROUPS,_ as FJALL_MANIFEST_FILENAME,w as MANIFEST_SCHEMA_VERSION,j as buildConstructMap,A as constructMapToRecord,T as deriveDisplayName,C as deriveGroupFromPath,v as enrichFromConstructMap,E as recordToConstructMap};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain helper: derive a content-addressed image tag from a buildx digest.
|
|
3
|
+
*
|
|
4
|
+
* Returns a tag of the shape `<serviceName>(-<target>)?-sha-<prefix>` where
|
|
5
|
+
* `prefix` is the first 12 lowercase-hex characters of the digest's sha256
|
|
6
|
+
* payload. The helper is the SOLE source of truth for content-hash tag
|
|
7
|
+
* synthesis — every producer (`@fjall/cli` `EcrBuildOrchestrator`,
|
|
8
|
+
* `@fjall/deploy-core` `dockerBuildHelper`) routes through it so a change
|
|
9
|
+
* to the format propagates without per-call-site drift.
|
|
10
|
+
*
|
|
11
|
+
* Rejection cases:
|
|
12
|
+
* - digest not prefixed with `sha256:` (we never call other algorithms)
|
|
13
|
+
* - digest hex payload shorter than 12 chars
|
|
14
|
+
* - serviceName empty or whitespace-only
|
|
15
|
+
* - target supplied as whitespace-only (caller would typically omit it
|
|
16
|
+
* entirely; `""` is treated as "no target", surfaced via a failure so
|
|
17
|
+
* the caller cannot pass through an upstream-empty value silently)
|
|
18
|
+
*
|
|
19
|
+
* The Result type below is structurally identical to the canonical
|
|
20
|
+
* `Result<T, E>` in `@fjall/generator/src/types/Result.ts`. `@fjall/util` is
|
|
21
|
+
* the root of the workspace dep graph, so we cannot import the canonical
|
|
22
|
+
* shape without creating a circular dependency. Consumers in
|
|
23
|
+
* `@fjall/deploy-core` and `@fjall/cli` consume values produced here and
|
|
24
|
+
* pass them through to canonical-`Result` consumers — structural identity
|
|
25
|
+
* means TS catches drift if the canonical shape ever changes.
|
|
26
|
+
*/
|
|
27
|
+
export type Result<T, E = Error> = {
|
|
28
|
+
success: true;
|
|
29
|
+
data: T;
|
|
30
|
+
} | {
|
|
31
|
+
success: false;
|
|
32
|
+
error: E;
|
|
33
|
+
};
|
|
34
|
+
export declare function deriveContentHashTag(digest: string, serviceName: string, target?: string): Result<string, Error>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function d(e){return{success:!0,data:e}}function t(e){return{success:!1,error:e}}const a="sha256:",o=12;function f(e,u,i){if(!e.startsWith(a))return t(new Error(`expected sha256:... digest, got ${e.slice(0,32)}`));const r=e.slice(a.length);if(r.length<o)return t(new Error(`digest hex payload too short (need >=${o} chars, got ${r.length})`));const n=u.trim();if(n==="")return t(new Error("serviceName must be a non-empty string"));let s;if(i!==void 0){const c=i.trim();if(c==="")return t(new Error("target, when supplied, must be a non-empty string; omit the argument instead"));s=c.toLowerCase()}const h=r.slice(0,o).toLowerCase(),m=s!==void 0?`${n.toLowerCase()}-${s}`:n.toLowerCase();return d(`${m}-sha-${h}`)}export{f as deriveContentHashTag};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-related implementations for `DockerCli`.
|
|
3
|
+
*
|
|
4
|
+
* `_buildxBuild` validates the args via `BuildxBuildArgsSchema.safeParse()`
|
|
5
|
+
* (Pitfall 4 — never `.parse()` inside a `Result`-returning function),
|
|
6
|
+
* spawns `docker buildx build` with the argv from the pure builder,
|
|
7
|
+
* line-buffers `--progress=rawjson` stdout into the rawjson parser, fires
|
|
8
|
+
* `onProgress` events through the masking boundary, then reads the
|
|
9
|
+
* `--metadata-file` for `containerimage.digest` to assemble the
|
|
10
|
+
* `BuildxBuildResult`.
|
|
11
|
+
*
|
|
12
|
+
* Resource discipline (per AC12 + robustness-standards § "Resource
|
|
13
|
+
* Acquisition Inside `try`"): the `--metadata-file` path is treated as a
|
|
14
|
+
* caller-owned-but-DockerCli-cleaned resource. `let
|
|
15
|
+
* tempMetadataPathForCleanup: string | undefined` outside the `try`,
|
|
16
|
+
* captured inside, removed via `rm(..., { force: true }).catch(...)` in
|
|
17
|
+
* `finally`. The `force: true` flag also short-circuits a missing-file
|
|
18
|
+
* cleanup which is the common case for failed-build paths where buildx
|
|
19
|
+
* never wrote the file.
|
|
20
|
+
*/
|
|
21
|
+
import { type BuildxBuildArgs, type BuildxBuildResult, type DockerCliError } from "./dockerCliSchemas.js";
|
|
22
|
+
import { type BuildxProgressEvent, type DockerCliState, type ImagetoolsInspect, type Result } from "./DockerCli.js";
|
|
23
|
+
export declare function _buildxBuild(state: DockerCliState, args: BuildxBuildArgs, onProgress: (event: BuildxProgressEvent) => void): Promise<Result<BuildxBuildResult, DockerCliError>>;
|
|
24
|
+
/**
|
|
25
|
+
* Create one or more manifest-list tags pointing at an existing image
|
|
26
|
+
* digest without re-uploading layers.
|
|
27
|
+
*
|
|
28
|
+
* Implementation: `docker buildx imagetools create --tag <tag1> --tag
|
|
29
|
+
* <tag2> ... <sourceImage>@<digest>`. The source `image@digest` reference
|
|
30
|
+
* pins the immutable content; each `--tag` argument creates (or moves) a
|
|
31
|
+
* mutable name pointing at that digest. The registry stores no new blobs,
|
|
32
|
+
* only a new manifest reference per tag.
|
|
33
|
+
*
|
|
34
|
+
* Discipline:
|
|
35
|
+
* - argv-only spawn (`shell: false` via `runDocker`)
|
|
36
|
+
* - tag values are validated to be non-empty strings before they enter argv
|
|
37
|
+
* - stderr is masked and bounded by `makeError(... stderrTail)`
|
|
38
|
+
* - timeout: re-uses `DEFAULT_INSPECT_TIMEOUT_MS`; imagetools create is a
|
|
39
|
+
* manifest-only operation comparable to inspect in latency
|
|
40
|
+
*/
|
|
41
|
+
export declare function _tagByDigest(state: DockerCliState, sourceImage: string, digest: string, tags: readonly string[]): Promise<Result<void, DockerCliError>>;
|
|
42
|
+
export declare function _imagetoolsInspect(state: DockerCliState, image: string): Promise<Result<ImagetoolsInspect, DockerCliError>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{rm as C}from"node:fs/promises";import{buildxArgvBuilder as D}from"./buildxArgvBuilder.js";import{BuildxBuildArgsSchema as k}from"./dockerCliSchemas.js";import{DEFAULT_BUILD_TIMEOUT_MS as $,DEFAULT_INSPECT_TIMEOUT_MS as w,DOCKER_CLI_BUILDX_LOG_CATEGORY as T}from"./dockerCliConstants.js";import{parseMetadataFile as B}from"./metadataFileParser.js";import{parseRawjsonLine as M}from"./rawjsonParser.js";import{rawjsonToVertexEvent as S}from"./rawjsonToVertexEvent.js";import{maskSensitiveOutput as x}from"../securityHelpers.js";import{failure as i,makeError as s,maskOutput as b,runDocker as _,spawnFailureToError as F,streamDocker as I,success as v}from"./DockerCli.js";function h(o){return o instanceof Error?o.message:String(o)}async function H(o,u,t){const r=k.safeParse(u);if(!r.success){const a=x(r.error.message);return i(s("validation",`Invalid BuildxBuildArgs: ${a}`,{issues:r.error.issues}))}const l=D(r.data);let n=0,g=0;const d=new Set,f=a=>{const m=M(a);if(m===null){a.trim()!==""&&o.logger.debug(T,"Skipping malformed rawjson line",{line:x(a)});return}const c=S(m);if(c!==null){for(const e of c.vertexes)e.completed!==void 0&&!d.has(e.digest)&&(d.add(e.digest),e.cached===!0?n++:g++),t({type:"vertex",message:b(e.name),vertex:e.digest});for(const e of c.logs)t({type:"log",message:b(e.data),vertex:e.vertex});for(const e of c.statuses)t({type:"status",message:b(`${e.name} ${e.current}`),vertex:e.vertex});for(const e of c.warnings)t({type:"warning",message:b(e.short),vertex:e.vertex})}};let p;try{p=r.data.metadataFile;const a=await I(o,{args:l,timeoutMs:$},f);if(a.spawnError!==void 0)return i(s("daemon_unreachable",`Docker CLI is not available: ${a.spawnError}`,{stderrTail:a.stderrTail}));if(a.aborted||a.exitCode===null)return i(s("abort","docker buildx build was aborted",{stderrTail:a.stderrTail}));if(a.exitCode!==0)return i(s("build_failed",`docker buildx build failed (exit ${a.exitCode})`,{stderrTail:a.stderrTail}));const m=await B(r.data.metadataFile);if(!m.success)return m;const c=m.data,e=c["containerimage.digest"];if(typeof e!="string"||e==="")return i(s("metadata_missing_digest","Buildx metadata file does not contain containerimage.digest",{metadataFile:r.data.metadataFile}));const y={};for(const E of r.data.tags)y[E]=e;return v({imageDigests:y,metadata:c,platforms:r.data.platforms,cacheHits:n,cacheMisses:g})}finally{p!==void 0&&await C(p,{force:!0}).catch(a=>{o.logger.warn(T,"Failed to clean up buildx metadata file",{path:p,error:x(h(a))})})}}async function K(o,u,t,r){if(u.trim()==="")return i(s("validation","tagByDigest: sourceImage must be non-empty"));if(!t.startsWith("sha256:"))return i(s("validation",`tagByDigest: expected sha256:... digest, got ${t.slice(0,32)}`));if(r.length===0)return i(s("validation","tagByDigest: tags must be a non-empty list"));for(const f of r)if(typeof f!="string"||f.trim()==="")return i(s("validation","tagByDigest: every tag must be non-empty"));const l=`${u}@${t}`,n=[];for(const f of r)n.push("--tag",f);const g=["buildx","imagetools","create",...n,l],d=await _(o,{args:g,timeoutMs:w});return d.spawnError!==void 0?i(F(d)):d.exitCode===null?i(s("abort",`docker buildx imagetools create for ${l} was aborted`,{stderrTail:d.stderrTail})):d.exitCode!==0?i(s("tag_failed",`docker buildx imagetools create for ${l} failed (exit ${d.exitCode})`,{stderrTail:d.stderrTail})):v(void 0)}async function V(o,u){const t=await _(o,{args:["buildx","imagetools","inspect",u,"--raw"],timeoutMs:w});if(t.spawnError!==void 0)return i(s("daemon_unreachable",`Docker CLI is not available: ${t.spawnError}`,{stderrTail:t.stderrTail}));if(t.exitCode===null)return i(s("abort",`docker buildx imagetools inspect ${u} was aborted`,{stderrTail:t.stderrTail}));if(t.exitCode!==0)return i(s("inspect_failed",`docker buildx imagetools inspect ${u} failed (exit ${t.exitCode})`,{stderrTail:t.stderrTail}));let r,l;try{const n=JSON.parse(t.stdout);typeof n.mediaType=="string"&&(r=n.mediaType),typeof n.digest=="string"&&(l=n.digest)}catch(n){o.logger.debug(T,"imagetools inspect output is not JSON; preserving raw",{error:x(h(n))})}return v({raw:t.stdout,...r!==void 0&&{mediaType:r},...l!==void 0&&{digest:l}})}export{H as _buildxBuild,V as _imagetoolsInspect,K as _tagByDigest};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concrete `DockerCli` class — wraps `docker` and `docker buildx` subprocess
|
|
3
|
+
* calls behind a typed interface. The class itself is thin: it carries shared
|
|
4
|
+
* state (logger, env, dockerBin, abort signal) and delegates each method to a
|
|
5
|
+
* pure-ish free function in a sibling file (`DockerCli.build.ts`,
|
|
6
|
+
* `DockerCli.registry.ts`, `DockerCli.daemon.ts`).
|
|
7
|
+
*
|
|
8
|
+
* Splitting was mandated by the plan's AC12 (each file < ~300 lines, one
|
|
9
|
+
* cohesion zone per file: build, registry, daemon).
|
|
10
|
+
*
|
|
11
|
+
* Subprocess discipline (every spawn call):
|
|
12
|
+
* - `shell: false`
|
|
13
|
+
* - `env` is `filterDangerousEnvVars(process.env)` plus any caller overrides
|
|
14
|
+
* - `onProgress` events are masked via `maskSensitiveOutput()` once at the
|
|
15
|
+
* boundary BEFORE the consumer callback fires
|
|
16
|
+
* - abort routed through `abortChildProcess()` from `./abortHelpers.js`
|
|
17
|
+
* - cleanup-side awaits in `finally` are wrapped with `.catch(...)` so
|
|
18
|
+
* cleanup failures cannot replace the operation result
|
|
19
|
+
*/
|
|
20
|
+
import { type ChildProcess } from "node:child_process";
|
|
21
|
+
import type { BuildxBuildArgs, BuildxBuildResult, DockerCliError, DockerCliErrorKind } from "./dockerCliSchemas.js";
|
|
22
|
+
import { failure, success, type Result } from "./result.js";
|
|
23
|
+
export interface DockerCliLogger {
|
|
24
|
+
debug(category: string, message: string, data?: Record<string, unknown>): void;
|
|
25
|
+
info(category: string, message: string, data?: Record<string, unknown>): void;
|
|
26
|
+
warn(category: string, message: string, data?: Record<string, unknown>): void;
|
|
27
|
+
error(category: string, message: string, data?: Record<string, unknown>): void;
|
|
28
|
+
}
|
|
29
|
+
export interface DockerCliOptions {
|
|
30
|
+
readonly logger: DockerCliLogger;
|
|
31
|
+
readonly dockerBin?: string;
|
|
32
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
33
|
+
readonly abortSignal?: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
export interface DockerCliState {
|
|
36
|
+
readonly logger: DockerCliLogger;
|
|
37
|
+
readonly dockerBin: string;
|
|
38
|
+
readonly env: NodeJS.ProcessEnv;
|
|
39
|
+
readonly abortSignal: AbortSignal | undefined;
|
|
40
|
+
}
|
|
41
|
+
export interface BuildxProgressEvent {
|
|
42
|
+
readonly type: "vertex" | "log" | "status" | "warning";
|
|
43
|
+
readonly message: string;
|
|
44
|
+
readonly vertex?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface PushProgressEvent {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
readonly status: string;
|
|
49
|
+
readonly current?: number;
|
|
50
|
+
readonly total?: number;
|
|
51
|
+
}
|
|
52
|
+
export interface PushResult {
|
|
53
|
+
readonly digest: string;
|
|
54
|
+
}
|
|
55
|
+
export interface PullProgressEvent {
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly status: string;
|
|
58
|
+
readonly current?: number;
|
|
59
|
+
readonly total?: number;
|
|
60
|
+
}
|
|
61
|
+
export interface PullResult {
|
|
62
|
+
readonly imageId: string;
|
|
63
|
+
}
|
|
64
|
+
export interface ImagetoolsInspect {
|
|
65
|
+
readonly raw: string;
|
|
66
|
+
readonly mediaType?: string;
|
|
67
|
+
readonly digest?: string;
|
|
68
|
+
}
|
|
69
|
+
export interface EcrLoginArgs {
|
|
70
|
+
readonly registry: string;
|
|
71
|
+
readonly username: string;
|
|
72
|
+
readonly password: string;
|
|
73
|
+
}
|
|
74
|
+
export interface BuildxCapabilities {
|
|
75
|
+
readonly version: string;
|
|
76
|
+
}
|
|
77
|
+
export interface DaemonInfo {
|
|
78
|
+
readonly serverName: string;
|
|
79
|
+
readonly serverVersion: string;
|
|
80
|
+
}
|
|
81
|
+
export declare class DockerCli {
|
|
82
|
+
private readonly state;
|
|
83
|
+
constructor(opts: DockerCliOptions);
|
|
84
|
+
buildxBuild(args: BuildxBuildArgs, onProgress: (event: BuildxProgressEvent) => void): Promise<Result<BuildxBuildResult, DockerCliError>>;
|
|
85
|
+
tag(source: string, target: string): Promise<Result<void, DockerCliError>>;
|
|
86
|
+
/**
|
|
87
|
+
* Create one or more manifest-list tags pointing at a previously pushed
|
|
88
|
+
* digest without re-uploading layers. Uses `docker buildx imagetools
|
|
89
|
+
* create` against `<sourceImage>@<digest>`.
|
|
90
|
+
*/
|
|
91
|
+
tagByDigest(sourceImage: string, digest: string, tags: readonly string[]): Promise<Result<void, DockerCliError>>;
|
|
92
|
+
push(image: string, onProgress?: (event: PushProgressEvent) => void): Promise<Result<PushResult, DockerCliError>>;
|
|
93
|
+
pull(image: string, platform?: string, onProgress?: (event: PullProgressEvent) => void): Promise<Result<PullResult, DockerCliError>>;
|
|
94
|
+
imageInspect(image: string): Promise<Result<{
|
|
95
|
+
exists: boolean;
|
|
96
|
+
digest?: string;
|
|
97
|
+
}, DockerCliError>>;
|
|
98
|
+
imagetoolsInspect(image: string): Promise<Result<ImagetoolsInspect, DockerCliError>>;
|
|
99
|
+
loginEcr(args: EcrLoginArgs): Promise<Result<void, DockerCliError>>;
|
|
100
|
+
ensureBuilder(name: string): Promise<Result<{
|
|
101
|
+
created: boolean;
|
|
102
|
+
}, DockerCliError>>;
|
|
103
|
+
assertBuildxAvailable(): Promise<Result<BuildxCapabilities, DockerCliError>>;
|
|
104
|
+
detectDaemon(): Promise<Result<DaemonInfo, DockerCliError>>;
|
|
105
|
+
}
|
|
106
|
+
export interface SpawnDockerOptions {
|
|
107
|
+
readonly args: readonly string[];
|
|
108
|
+
readonly stdin?: string;
|
|
109
|
+
readonly timeoutMs?: number;
|
|
110
|
+
}
|
|
111
|
+
export interface SpawnDockerResult {
|
|
112
|
+
readonly exitCode: number | null;
|
|
113
|
+
readonly stdout: string;
|
|
114
|
+
readonly stderr: string;
|
|
115
|
+
readonly stderrTail: readonly string[];
|
|
116
|
+
readonly aborted: boolean;
|
|
117
|
+
readonly spawnError?: string;
|
|
118
|
+
}
|
|
119
|
+
export declare function makeError(kind: DockerCliErrorKind, message: string, details?: Record<string, unknown>): DockerCliError;
|
|
120
|
+
export declare function spawnFailureToError(result: {
|
|
121
|
+
spawnError?: string;
|
|
122
|
+
stderrTail: readonly string[];
|
|
123
|
+
}): DockerCliError;
|
|
124
|
+
export declare function tailLines(buffer: string, count: number): string[];
|
|
125
|
+
export declare function maskOutput(value: string): string;
|
|
126
|
+
export declare function runDocker(state: DockerCliState, opts: SpawnDockerOptions): Promise<SpawnDockerResult>;
|
|
127
|
+
export declare function streamDocker(state: DockerCliState, opts: SpawnDockerOptions, onStdoutLine: (line: string) => void, onStderrLine?: (line: string) => void): Promise<{
|
|
128
|
+
exitCode: number | null;
|
|
129
|
+
stderrTail: readonly string[];
|
|
130
|
+
aborted: boolean;
|
|
131
|
+
spawnError?: string;
|
|
132
|
+
child: ChildProcess | null;
|
|
133
|
+
}>;
|
|
134
|
+
export { failure, success };
|
|
135
|
+
export type { Result };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon + builder bootstrap implementations for `DockerCli`.
|
|
3
|
+
*
|
|
4
|
+
* `assertBuildxAvailable` reads `docker buildx version` stdout and rejects
|
|
5
|
+
* any version below `BUILDX_VERSION_FLOOR`. The error path distinguishes
|
|
6
|
+
* "buildx plugin missing" (engine present, plugin exit 127) from
|
|
7
|
+
* "docker daemon unreachable" (engine command itself fails) so the
|
|
8
|
+
* surface error message can offer a targeted install hint.
|
|
9
|
+
*
|
|
10
|
+
* `detectDaemon` runs `docker version --format json` and surfaces the
|
|
11
|
+
* server name; Podman returns `Podman Engine` and is rejected fail-fast
|
|
12
|
+
* with `kind: "buildx_unavailable"`.
|
|
13
|
+
*
|
|
14
|
+
* `ensureBuilder` checks for the named buildx builder and creates it on
|
|
15
|
+
* absence. A wrong-driver builder (`docker` instead of `docker-container`)
|
|
16
|
+
* is removed and re-created; consumer is logged a warning.
|
|
17
|
+
*/
|
|
18
|
+
import type { DockerCliError } from "./dockerCliSchemas.js";
|
|
19
|
+
import { type BuildxCapabilities, type DaemonInfo, type DockerCliState, type Result } from "./DockerCli.js";
|
|
20
|
+
export declare function _assertBuildxAvailable(state: DockerCliState): Promise<Result<BuildxCapabilities, DockerCliError>>;
|
|
21
|
+
export declare function _detectDaemon(state: DockerCliState): Promise<Result<DaemonInfo, DockerCliError>>;
|
|
22
|
+
export declare function _ensureBuilder(state: DockerCliState, name: string): Promise<Result<{
|
|
23
|
+
created: boolean;
|
|
24
|
+
}, DockerCliError>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{maskSensitiveOutput as p}from"../securityHelpers.js";import{BUILDX_VERSION_FLOOR as l,DEFAULT_DAEMON_PROBE_TIMEOUT_MS as x,DEFAULT_INSPECT_TIMEOUT_MS as c,DOCKER_CLI_BUILDX_LOG_CATEGORY as _,DOCKER_CLI_LOG_CATEGORY as g}from"./dockerCliConstants.js";import{failure as t,makeError as u,runDocker as s,spawnFailureToError as d,success as b}from"./DockerCli.js";function v(n){const e=n.startsWith("v")?n.slice(1):n,r=/^(\d+)\.(\d+)\.(\d+)/.exec(e);return r===null?null:[parseInt(r[1]??"0",10),parseInt(r[2]??"0",10),parseInt(r[3]??"0",10)]}function T(n,e){for(let r=0;r<3;r++){const i=n[r]??0,o=e[r]??0;if(i!==o)return i-o}return 0}function E(n){const e=/github\.com\/docker\/buildx\s+(v?\d+\.\d+\.\d+)/.exec(n);return e!==null&&e[1]!==void 0?e[1]:/(\d+\.\d+\.\d+)/.exec(n)?.[1]??null}async function O(n){const e=await s(n,{args:["buildx","version"],timeoutMs:c});if(e.spawnError!==void 0)return t(d(e));if(e.exitCode===null)return t(u("abort","docker buildx version was aborted",{stderrTail:e.stderrTail}));if(e.exitCode!==0){const a=await s(n,{args:["version","--format","{{.Server.Version}}"],timeoutMs:x});return a.spawnError!==void 0?t(d(a)):a.exitCode!==0?t(u("daemon_unreachable","Docker daemon is not running or not installed",{stderrTail:a.stderrTail})):t(u("buildx_unavailable",`Docker Buildx plugin is not installed (need >= ${l})`,{stderrTail:e.stderrTail}))}const r=E(e.stdout);if(r===null)return t(u("buildx_unavailable","Could not parse docker buildx version output",{stdout:e.stdout}));const i=v(r),o=v(l);return i===null||o===null?t(u("buildx_unavailable",`Could not parse buildx version "${r}"`,{stdout:e.stdout})):T(i,o)<0?t(u("buildx_unavailable",`Docker Buildx ${r} is below the required ${l} floor`,{observed:r,floor:l})):(n.logger.debug(_,"buildx available",{version:r}),b({version:r}))}async function D(n){const e=await s(n,{args:["version","--format","json"],timeoutMs:x});if(e.spawnError!==void 0)return t(d(e));if(e.exitCode===null)return t(u("abort","docker version was aborted",{stderrTail:e.stderrTail}));if(e.exitCode!==0)return t(u("daemon_unreachable","Docker daemon is not running or not installed",{stderrTail:e.stderrTail}));let r;try{r=JSON.parse(e.stdout)}catch(f){const m=p(f instanceof Error?f.message:String(f));return t(u("daemon_unreachable",`docker version output is not valid JSON: ${m}`,{stdout:e.stdout}))}if(r===null||typeof r!="object")return t(u("daemon_unreachable","docker version did not return a JSON object"));const i=r,o=i.Server?.Name??"Docker",a=i.Server?.Version??"unknown";return o==="Podman Engine"?t(u("buildx_unavailable","Podman is not supported; install Docker Engine 23+ with the buildx plugin")):b({serverName:o,serverVersion:a})}async function S(n,e){const r=await s(n,{args:["buildx","inspect",e],timeoutMs:c});if(r.spawnError!==void 0)return t(d(r));if(r.exitCode===0){if(/Driver:\s*docker-container/.test(r.stdout))return b({created:!1});n.logger.warn(g,"Replacing buildx builder with wrong driver",{name:e});const o=await s(n,{args:["buildx","rm",e],timeoutMs:c});if(o.spawnError!==void 0)return t(d(o));if(o.exitCode!==0)return t(u("buildx_unavailable",`Failed to remove existing buildx builder ${e}`,{stderrTail:o.stderrTail}))}const i=await s(n,{args:["buildx","create","--name",e,"--driver","docker-container","--use"],timeoutMs:c});return i.spawnError!==void 0?t(d(i)):i.exitCode!==0?t(u("buildx_unavailable",`Failed to create buildx builder ${e}`,{stderrTail:i.stderrTail})):b({created:!0})}export{O as _assertBuildxAvailable,D as _detectDaemon,S as _ensureBuilder};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{spawn as b}from"node:child_process";import{filterDangerousEnvVars as B,maskSensitiveOutput as E}from"../securityHelpers.js";import{abortChildProcess as h}from"./abortHelpers.js";import{DEFAULT_DOCKER_BIN as k,DOCKER_CLI_LOG_CATEGORY as A,STDERR_TAIL_LINE_MAX_CHARS as y,STDERR_TAIL_LINES as _}from"./dockerCliConstants.js";import{_buildxBuild as x,_imagetoolsInspect as D,_tagByDigest as w}from"./DockerCli.build.js";import{_assertBuildxAvailable as C,_detectDaemon as I,_ensureBuilder as L}from"./DockerCli.daemon.js";import{_imageInspect as R,_loginEcr as O,_pull as P,_push as N,_tag as F}from"./DockerCli.registry.js";import{failure as G,success as K}from"./result.js";class M{state;constructor(e){const r=e.dockerBin!==void 0&&e.dockerBin!==""?e.dockerBin:k,n=B(e.env??process.env);this.state={logger:e.logger,dockerBin:r,env:n,abortSignal:e.abortSignal}}async buildxBuild(e,r){return x(this.state,e,r)}async tag(e,r){return F(this.state,e,r)}async tagByDigest(e,r,n){return w(this.state,e,r,n)}async push(e,r){return N(this.state,e,r)}async pull(e,r,n){return P(this.state,e,r,n)}async imageInspect(e){return R(this.state,e)}async imagetoolsInspect(e){return D(this.state,e)}async loginEcr(e){return O(this.state,e)}async ensureBuilder(e){return L(this.state,e)}async assertBuildxAvailable(){return C(this.state)}async detectDaemon(){return I(this.state)}}function j(t){const e=E(t);return e.length>y?e.slice(0,y)+"\u2026":e}function H(t,e,r){let n,l=r;if(r!==void 0&&Array.isArray(r.stderrTail)&&r.stderrTail.every(o=>typeof o=="string")){n=r.stderrTail.map(j);const{stderrTail:o,...s}=r;l=Object.keys(s).length>0?s:void 0}return{kind:t,message:e,...n!==void 0&&{stderrTail:n},...l!==void 0&&{details:l}}}function Q(t){return H("daemon_unreachable",`Docker CLI is not available: ${t.spawnError??"unknown spawn failure"}`,{stderrTail:t.stderrTail})}function v(t,e){if(t==="")return[];const r=t.split(/\r?\n/);for(;r.length>0&&r[r.length-1]==="";)r.pop();return r.slice(-e)}function W(t){return E(t)}function Z(t,e){return new Promise(r=>{let n=!1,l=!1;const o=T(t.abortSignal,e.timeoutMs);let s;try{s=b(t.dockerBin,e.args,{shell:!1,env:t.env})}catch(i){const a=i instanceof Error?i.message:String(i);r({exitCode:null,stdout:"",stderr:a,stderrTail:[a],aborted:!1,spawnError:a});return}let c="",u="";s.stdout?.on("data",i=>{c+=i.toString()}),s.stderr?.on("data",i=>{u+=i.toString()}),e.stdin!==void 0&&(s.stdin?.on("error",i=>{t.logger.debug(A,"stdin write error (typically EPIPE on early child exit)",{error:i.message})}),s.stdin?.write(e.stdin),s.stdin?.end());const g=()=>{n=!0,h(s)};o!==void 0&&(o.aborted?g():o.addEventListener("abort",g,{once:!0})),s.once("error",i=>{if(l)return;l=!0,o!==void 0&&o.removeEventListener("abort",g);const a=i instanceof Error?i.message:String(i);r({exitCode:null,stdout:c,stderr:a,stderrTail:[a],aborted:n,spawnError:a})}),s.once("close",i=>{l||(l=!0,o!==void 0&&o.removeEventListener("abort",g),r({exitCode:i,stdout:c,stderr:u,stderrTail:v(u,_),aborted:n}))})})}function ee(t,e,r,n){return new Promise(l=>{let o=!1,s=!1;const c=T(t.abortSignal,e.timeoutMs);let u;try{u=b(t.dockerBin,e.args,{shell:!1,env:t.env})}catch(d){const f=d instanceof Error?d.message:String(d);l({exitCode:null,stderrTail:[f],aborted:!1,spawnError:f,child:null});return}let g="",i="",a="";u.stdout?.on("data",d=>{g+=d.toString();const f=g.split(/\r?\n/);g=f.pop()??"";for(const p of f)r(p)}),u.stderr?.on("data",d=>{const f=d.toString();if(i+=f,n!==void 0){a+=f;const p=a.split(/\r?\n/);a=p.pop()??"";for(const S of p)n(S)}});const m=()=>{o=!0,h(u)};c!==void 0&&(c.aborted?m():c.addEventListener("abort",m,{once:!0})),u.once("error",d=>{if(s)return;s=!0,c!==void 0&&c.removeEventListener("abort",m);const f=d instanceof Error?d.message:String(d);l({exitCode:null,stderrTail:[f],aborted:o,spawnError:f,child:u})}),u.once("close",d=>{s||(s=!0,g!==""&&r(g),n!==void 0&&a!==""&&n(a),c!==void 0&&c.removeEventListener("abort",m),l({exitCode:d,stderrTail:v(i,_),aborted:o,child:u}))})})}function T(t,e){if(!(t===void 0&&e===void 0))return t===void 0?AbortSignal.timeout(e):e===void 0?t:AbortSignal.any([t,AbortSignal.timeout(e)])}export{M as DockerCli,G as failure,H as makeError,W as maskOutput,Z as runDocker,Q as spawnFailureToError,ee as streamDocker,K as success,v as tailLines};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry-related implementations for `DockerCli`.
|
|
3
|
+
*
|
|
4
|
+
* `loginEcr` shells `docker login --username <u> --password-stdin <registry>`
|
|
5
|
+
* with the password sent on stdin so it never appears in argv (visible via
|
|
6
|
+
* `ps`). `imageInspect` distinguishes "image not found" (kind:inspect_failed
|
|
7
|
+
* with `exists: false`) from other inspect failures by sniffing the
|
|
8
|
+
* stderr tail for the literal `No such image:` substring docker emits.
|
|
9
|
+
*/
|
|
10
|
+
import type { DockerCliError } from "./dockerCliSchemas.js";
|
|
11
|
+
import { type DockerCliState, type EcrLoginArgs, type PullProgressEvent, type PullResult, type PushProgressEvent, type PushResult, type Result } from "./DockerCli.js";
|
|
12
|
+
export declare function _tag(state: DockerCliState, source: string, target: string): Promise<Result<void, DockerCliError>>;
|
|
13
|
+
export declare function _push(state: DockerCliState, image: string, onProgress?: (event: PushProgressEvent) => void): Promise<Result<PushResult, DockerCliError>>;
|
|
14
|
+
export declare function _pull(state: DockerCliState, image: string, platform?: string, onProgress?: (event: PullProgressEvent) => void): Promise<Result<PullResult, DockerCliError>>;
|
|
15
|
+
export declare function _imageInspect(state: DockerCliState, image: string): Promise<Result<{
|
|
16
|
+
exists: boolean;
|
|
17
|
+
digest?: string;
|
|
18
|
+
}, DockerCliError>>;
|
|
19
|
+
export declare function _loginEcr(state: DockerCliState, args: EcrLoginArgs): Promise<Result<void, DockerCliError>>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{maskSensitiveOutput as h}from"../securityHelpers.js";import{DEFAULT_DAEMON_PROBE_TIMEOUT_MS as E,DEFAULT_INSPECT_TIMEOUT_MS as x,DEFAULT_PULL_TIMEOUT_MS as w,DEFAULT_PUSH_TIMEOUT_MS as C,DOCKER_CLI_LOG_CATEGORY as $}from"./dockerCliConstants.js";import{failure as i,makeError as a,maskOutput as _,runDocker as T,spawnFailureToError as c,streamDocker as g,success as u}from"./DockerCli.js";async function M(o,r,e){const t=await T(o,{args:["tag",r,e],timeoutMs:x});return t.spawnError!==void 0?i(c(t)):t.exitCode===null?i(a("abort",`docker tag ${r} ${e} was aborted`,{stderrTail:t.stderrTail})):t.exitCode!==0?i(a("tag_failed",`docker tag ${r} ${e} failed (exit ${t.exitCode})`,{stderrTail:t.stderrTail})):u(void 0)}async function D(o,r,e){let t;const d=await g(o,{args:["push",r],timeoutMs:C},f=>{if(f==="")return;let s;try{s=JSON.parse(f)}catch{o.logger.debug($,"Skipping malformed push progress line",{line:h(f)});return}if(s===null||typeof s!="object")return;const n=s;n.aux?.digest!==void 0&&(t=n.aux.digest),e!==void 0&&n.id!==void 0&&n.status!==void 0&&e({id:n.id,status:_(n.status),...n.progressDetail?.current!==void 0&&{current:n.progressDetail.current},...n.progressDetail?.total!==void 0&&{total:n.progressDetail.total}})});return d.spawnError!==void 0?i(c(d)):d.aborted||d.exitCode===null?i(a("abort",`docker push ${r} was aborted`,{stderrTail:d.stderrTail})):d.exitCode!==0?d.stderrTail.join(`
|
|
2
|
+
`).toLowerCase().includes("unauthorized")?i(a("auth_failed",`docker push ${r} unauthorized`,{stderrTail:d.stderrTail})):i(a("push_failed",`docker push ${r} failed (exit ${d.exitCode})`,{stderrTail:d.stderrTail})):t===void 0?i(a("push_failed",`docker push ${r} succeeded but no digest was reported`,{stderrTail:d.stderrTail})):u({digest:t})}async function L(o,r,e,t){const l=["pull"];e!==void 0&&e!==""&&l.push("--platform",e),l.push(r);let d="";const s=await g(o,{args:l,timeoutMs:w},p=>{d+=`${p}
|
|
3
|
+
`,!(t===void 0||p==="")&&t({id:r,status:_(p)})});if(s.spawnError!==void 0)return i(c(s));if(s.aborted||s.exitCode===null)return i(a("abort",`docker pull ${r} was aborted`,{stderrTail:s.stderrTail}));if(s.exitCode!==0)return i(a("pull_failed",`docker pull ${r} failed (exit ${s.exitCode})`,{stderrTail:s.stderrTail}));const n=/sha256:[0-9a-f]{64}/.exec(d);return u({imageId:n?.[0]??r})}async function O(o,r){const e=await T(o,{args:["image","inspect","--format","{{json .}}",r],timeoutMs:x});if(e.spawnError!==void 0)return i(c(e));if(e.exitCode===null)return i(a("abort",`docker image inspect ${r} was aborted`,{stderrTail:e.stderrTail}));if(e.exitCode!==0)return e.stderr.includes("No such image:")?u({exists:!1}):i(a("inspect_failed",`docker image inspect ${r} failed (exit ${e.exitCode})`,{stderrTail:e.stderrTail}));const t=/"Id":"(sha256:[0-9a-f]{64})"/.exec(e.stdout);return u({exists:!0,...t?.[1]!==void 0&&{digest:t[1]}})}async function y(o,r){const e=await T(o,{args:["login","--username",r.username,"--password-stdin",r.registry],stdin:r.password,timeoutMs:E});return e.spawnError!==void 0?i(c(e)):e.exitCode===null?i(a("abort",`docker login ${r.registry} was aborted`)):e.exitCode!==0?i(a("auth_failed",`docker login ${r.registry} failed (exit ${e.exitCode})`,{stderrTail:e.stderrTail})):u(void 0)}export{O as _imageInspect,y as _loginEcr,L as _pull,D as _push,M as _tag};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform abort for spawned `docker` / `docker buildx` processes.
|
|
3
|
+
*
|
|
4
|
+
* `child_process.spawn(...).kill('SIGTERM')` on Windows translates to
|
|
5
|
+
* `TerminateProcess` — equivalent to SIGKILL, with no grace window. We
|
|
6
|
+
* use `taskkill /T /F /PID` instead so child trees (`docker` → buildkitd)
|
|
7
|
+
* are torn down explicitly. POSIX gets the standard SIGTERM-then-SIGKILL
|
|
8
|
+
* grace pattern.
|
|
9
|
+
*
|
|
10
|
+
* `platform` is injected so tests pass `"win32"` directly without
|
|
11
|
+
* `vi.stubGlobal('process', ...)`, which is fragile and leaks across
|
|
12
|
+
* tests.
|
|
13
|
+
*
|
|
14
|
+
* Stream cleanup before signalling per robustness-standards § "Process
|
|
15
|
+
* Timeout with Stream Cleanup".
|
|
16
|
+
*/
|
|
17
|
+
import { type ChildProcess } from "node:child_process";
|
|
18
|
+
export declare function abortChildProcess(child: ChildProcess, platform?: NodeJS.Platform): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{spawnSync as o}from"node:child_process";import{SIGTERM_GRACE_MS as s}from"./dockerCliConstants.js";function f(t,r=process.platform){if(t.pid===void 0)return;if(t.stdout&&t.stdout.destroy(),t.stderr&&t.stderr.destroy(),r==="win32"){o("taskkill",["/T","/F","/PID",String(t.pid)],{shell:!1});return}t.kill("SIGTERM");const e=setTimeout(()=>{t.killed||t.kill("SIGKILL")},s);t.once("exit",()=>clearTimeout(e))}export{f as abortChildProcess};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure function: `BuildxBuildArgs` → `string[]`.
|
|
3
|
+
*
|
|
4
|
+
* No closure state, no env reads, no subprocess calls, no I/O. The input
|
|
5
|
+
* is validated upstream by `BuildxBuildArgsSchema.safeParse()`; this
|
|
6
|
+
* function trusts its parameter shape and only emits argv tokens.
|
|
7
|
+
*
|
|
8
|
+
* Argv ordering matches the design §4.1 spec verbatim. Builder, progress,
|
|
9
|
+
* metadata-file, platform, tags, dockerfile, build-args, target, cache,
|
|
10
|
+
* provenance, sbom, push|load, then context path. Pure-function discipline
|
|
11
|
+
* is enforced by `buildxArgvBuilder.test.ts` (byte-identical output for
|
|
12
|
+
* the same input on repeat calls).
|
|
13
|
+
*/
|
|
14
|
+
import type { BuildxBuildArgs } from "./dockerCliSchemas.js";
|
|
15
|
+
export declare function buildxArgvBuilder(args: BuildxBuildArgs): string[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{DEFAULT_BUILDER_NAME as f}from"./dockerCliConstants.js";function i(e){const o=["buildx","build"];o.push("--builder",e.builder??f),o.push("--progress=rawjson"),o.push("--metadata-file",e.metadataFile),o.push("--platform",e.platforms.join(","));for(const t of e.tags)o.push("-t",t);o.push("-f",e.dockerfilePath);for(const[t,u]of Object.entries(e.buildArgs))o.push("--build-arg",`${t}=${u}`);if(e.target!==void 0&&o.push("--target",e.target),e.cacheFrom!==void 0)for(const t of e.cacheFrom)o.push("--cache-from",t);if(e.cacheTo!==void 0)for(const t of e.cacheTo)o.push("--cache-to",t);if(e.secrets!==void 0)for(const t of e.secrets)o.push("--secret",`id=${t.id},src=${t.source}`);return o.push(`--provenance=${e.provenance?"true":"false"}`),o.push(`--sbom=${e.sbom?"true":"false"}`),e.push&&o.push("--push"),e.load&&o.push("--load"),o.push(e.contextPath),o}export{i as buildxArgvBuilder};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const DOCKER_CLI_LOG_CATEGORY = "DockerCli";
|
|
2
|
+
export declare const DOCKER_CLI_BUILDX_LOG_CATEGORY = "DockerCli.buildx";
|
|
3
|
+
export declare const BUILDX_VERSION_FLOOR = "0.13.0";
|
|
4
|
+
export declare const ENGINE_VERSION_FLOOR = "23.0.0";
|
|
5
|
+
export declare const PrerequisiteMissingExitCode = 64;
|
|
6
|
+
export declare const DEFAULT_BUILDER_NAME = "fjall";
|
|
7
|
+
export declare const DEFAULT_DOCKER_BIN = "docker";
|
|
8
|
+
export declare const SIGTERM_GRACE_MS = 5000;
|
|
9
|
+
export declare const STDERR_TAIL_LINES = 50;
|
|
10
|
+
export declare const STDERR_TAIL_LINE_MAX_CHARS = 2000;
|
|
11
|
+
export declare const DEFAULT_BUILD_TIMEOUT_MS = 600000;
|
|
12
|
+
export declare const DEFAULT_PUSH_TIMEOUT_MS = 300000;
|
|
13
|
+
export declare const DEFAULT_PULL_TIMEOUT_MS = 300000;
|
|
14
|
+
export declare const DEFAULT_INSPECT_TIMEOUT_MS = 30000;
|
|
15
|
+
export declare const DEFAULT_DAEMON_PROBE_TIMEOUT_MS = 10000;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const _="DockerCli",o="DockerCli.buildx",E="0.13.0",t="23.0.0",T=64,L="fjall",e="docker",I=5e3,r=50,O=2e3,D=6e5,c=3e5,s=3e5,R=3e4,U=1e4;export{E as BUILDX_VERSION_FLOOR,L as DEFAULT_BUILDER_NAME,D as DEFAULT_BUILD_TIMEOUT_MS,U as DEFAULT_DAEMON_PROBE_TIMEOUT_MS,e as DEFAULT_DOCKER_BIN,R as DEFAULT_INSPECT_TIMEOUT_MS,s as DEFAULT_PULL_TIMEOUT_MS,c as DEFAULT_PUSH_TIMEOUT_MS,o as DOCKER_CLI_BUILDX_LOG_CATEGORY,_ as DOCKER_CLI_LOG_CATEGORY,t as ENGINE_VERSION_FLOOR,T as PrerequisiteMissingExitCode,I as SIGTERM_GRACE_MS,r as STDERR_TAIL_LINES,O as STDERR_TAIL_LINE_MAX_CHARS};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas + companion types for the DockerCli boundary contract.
|
|
3
|
+
*
|
|
4
|
+
* Every cross-process boundary (CLI ↔ webapp worker, subprocess ↔ adapter)
|
|
5
|
+
* round-trips through these schemas. Pitfall guards encoded:
|
|
6
|
+
* - Pitfall 1 (schema-interface drift): every TS type is `z.infer<typeof Schema>`
|
|
7
|
+
* - Pitfall 3 (.strict() on every z.object()): all object schemas use .strict() before refinements
|
|
8
|
+
* - Pitfall 4 (.parse() inside Result): callers use safeParse() and map to failure({...})
|
|
9
|
+
* - Pitfall 6 (companion type pairing): every export pairs schema + type in the same statement
|
|
10
|
+
* - Pitfall 7 (mutable .default()): no schema uses .default({}) or .default([])
|
|
11
|
+
* - Pitfall 9 (.optional() string + empty-string trap): `target` uses .min(1).optional()
|
|
12
|
+
*
|
|
13
|
+
* `BuildxBuildArgs` is the argv-builder contract — flat fields that map 1:1
|
|
14
|
+
* to `docker buildx build` flags. The role-aware manifest contract
|
|
15
|
+
* (`DockerBuild { path, context?, target? }`) lives in `@fjall/util/manifest`
|
|
16
|
+
* and is projected into this shape by a thin caller-side adapter
|
|
17
|
+
* (`dockerBuildToBuildxArgs`) in PR 2's CLI orchestrator.
|
|
18
|
+
*/
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
export declare const DockerCliErrorKindSchema: z.ZodEnum<{
|
|
21
|
+
abort: "abort";
|
|
22
|
+
validation: "validation";
|
|
23
|
+
buildx_unavailable: "buildx_unavailable";
|
|
24
|
+
daemon_unreachable: "daemon_unreachable";
|
|
25
|
+
build_failed: "build_failed";
|
|
26
|
+
push_failed: "push_failed";
|
|
27
|
+
pull_failed: "pull_failed";
|
|
28
|
+
tag_failed: "tag_failed";
|
|
29
|
+
inspect_failed: "inspect_failed";
|
|
30
|
+
auth_failed: "auth_failed";
|
|
31
|
+
timeout: "timeout";
|
|
32
|
+
metadata_missing: "metadata_missing";
|
|
33
|
+
metadata_malformed: "metadata_malformed";
|
|
34
|
+
metadata_missing_digest: "metadata_missing_digest";
|
|
35
|
+
}>;
|
|
36
|
+
export type DockerCliErrorKind = z.infer<typeof DockerCliErrorKindSchema>;
|
|
37
|
+
export declare function isDockerCliErrorKind(value: string): value is DockerCliErrorKind;
|
|
38
|
+
export declare const BuildxBuildArgsSchema: z.ZodObject<{
|
|
39
|
+
contextPath: z.ZodString;
|
|
40
|
+
dockerfilePath: z.ZodString;
|
|
41
|
+
target: z.ZodOptional<z.ZodString>;
|
|
42
|
+
platforms: z.ZodArray<z.ZodString>;
|
|
43
|
+
tags: z.ZodArray<z.ZodString>;
|
|
44
|
+
buildArgs: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
45
|
+
cacheFrom: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
46
|
+
cacheTo: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
47
|
+
secrets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
48
|
+
id: z.ZodString;
|
|
49
|
+
source: z.ZodString;
|
|
50
|
+
}, z.core.$strict>>>;
|
|
51
|
+
provenance: z.ZodBoolean;
|
|
52
|
+
sbom: z.ZodBoolean;
|
|
53
|
+
push: z.ZodBoolean;
|
|
54
|
+
load: z.ZodBoolean;
|
|
55
|
+
builder: z.ZodOptional<z.ZodString>;
|
|
56
|
+
metadataFile: z.ZodString;
|
|
57
|
+
}, z.core.$strict>;
|
|
58
|
+
export type BuildxBuildArgs = z.infer<typeof BuildxBuildArgsSchema>;
|
|
59
|
+
export declare const BuildxBuildResultSchema: z.ZodObject<{
|
|
60
|
+
imageDigests: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
61
|
+
metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
62
|
+
platforms: z.ZodArray<z.ZodString>;
|
|
63
|
+
cacheHits: z.ZodNumber;
|
|
64
|
+
cacheMisses: z.ZodNumber;
|
|
65
|
+
}, z.core.$strict>;
|
|
66
|
+
export type BuildxBuildResult = z.infer<typeof BuildxBuildResultSchema>;
|
|
67
|
+
export declare const DockerCliErrorSchema: z.ZodObject<{
|
|
68
|
+
kind: z.ZodEnum<{
|
|
69
|
+
abort: "abort";
|
|
70
|
+
validation: "validation";
|
|
71
|
+
buildx_unavailable: "buildx_unavailable";
|
|
72
|
+
daemon_unreachable: "daemon_unreachable";
|
|
73
|
+
build_failed: "build_failed";
|
|
74
|
+
push_failed: "push_failed";
|
|
75
|
+
pull_failed: "pull_failed";
|
|
76
|
+
tag_failed: "tag_failed";
|
|
77
|
+
inspect_failed: "inspect_failed";
|
|
78
|
+
auth_failed: "auth_failed";
|
|
79
|
+
timeout: "timeout";
|
|
80
|
+
metadata_missing: "metadata_missing";
|
|
81
|
+
metadata_malformed: "metadata_malformed";
|
|
82
|
+
metadata_missing_digest: "metadata_missing_digest";
|
|
83
|
+
}>;
|
|
84
|
+
message: z.ZodString;
|
|
85
|
+
stderrTail: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
86
|
+
details: z.ZodOptional<z.ZodUnknown>;
|
|
87
|
+
}, z.core.$strict>;
|
|
88
|
+
export type DockerCliError = z.infer<typeof DockerCliErrorSchema>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{z as t}from"zod";const a=t.enum(["buildx_unavailable","daemon_unreachable","build_failed","push_failed","pull_failed","tag_failed","inspect_failed","auth_failed","abort","timeout","validation","metadata_missing","metadata_malformed","metadata_missing_digest"]),e=new Set(a.options);function r(i){return e.has(i)}const o=t.object({contextPath:t.string().min(1),dockerfilePath:t.string().min(1),target:t.string().min(1).optional(),platforms:t.array(t.string().min(1)).min(1),tags:t.array(t.string().min(1)).min(1),buildArgs:t.record(t.string(),t.string()),cacheFrom:t.array(t.string().min(1)).optional(),cacheTo:t.array(t.string().min(1)).optional(),secrets:t.array(t.object({id:t.string().min(1),source:t.string().min(1)}).strict()).optional(),provenance:t.boolean(),sbom:t.boolean(),push:t.boolean(),load:t.boolean(),builder:t.string().min(1,"Builder name cannot be empty").optional(),metadataFile:t.string().min(1)}).strict().refine(i=>!(i.platforms.length>1&&i.load),{message:"Multi-arch builds cannot use load:true (Docker limitation); use push:true or omit both for cache-only"}).refine(i=>!(i.push&&i.load),{message:"push and load are mutually exclusive"}),s=t.object({imageDigests:t.record(t.string(),t.string()),metadata:t.record(t.string(),t.unknown()),platforms:t.array(t.string()),cacheHits:t.number().int().nonnegative(),cacheMisses:t.number().int().nonnegative()}).strict(),l=t.object({kind:a,message:t.string(),stderrTail:t.array(t.string()).optional(),details:t.unknown().optional()}).strict();export{o as BuildxBuildArgsSchema,s as BuildxBuildResultSchema,a as DockerCliErrorKindSchema,l as DockerCliErrorSchema,r as isDockerCliErrorKind};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { type Result, isSuccess, isFailure, success, failure } from "./result.js";
|
|
2
|
+
export { DOCKER_CLI_LOG_CATEGORY, DOCKER_CLI_BUILDX_LOG_CATEGORY, BUILDX_VERSION_FLOOR, ENGINE_VERSION_FLOOR, PrerequisiteMissingExitCode, DEFAULT_BUILDER_NAME, DEFAULT_DOCKER_BIN, SIGTERM_GRACE_MS, STDERR_TAIL_LINES, DEFAULT_BUILD_TIMEOUT_MS, DEFAULT_PUSH_TIMEOUT_MS, DEFAULT_PULL_TIMEOUT_MS, DEFAULT_INSPECT_TIMEOUT_MS, DEFAULT_DAEMON_PROBE_TIMEOUT_MS } from "./dockerCliConstants.js";
|
|
3
|
+
export { BuildxBuildArgsSchema, BuildxBuildResultSchema, DockerCliErrorKindSchema, DockerCliErrorSchema, isDockerCliErrorKind, type BuildxBuildArgs, type BuildxBuildResult, type DockerCliError, type DockerCliErrorKind } from "./dockerCliSchemas.js";
|
|
4
|
+
export { buildxArgvBuilder } from "./buildxArgvBuilder.js";
|
|
5
|
+
export { parseRawjsonLine, type RawjsonEnvelope, type RawjsonVertex, type RawjsonStatus, type RawjsonLog, type RawjsonWarning } from "./rawjsonParser.js";
|
|
6
|
+
export { rawjsonToVertexEvent, type RawjsonVertexEvent, type NormalisedVertex, type NormalisedStatus, type NormalisedLog, type NormalisedWarning } from "./rawjsonToVertexEvent.js";
|
|
7
|
+
export { parseMetadataFile } from "./metadataFileParser.js";
|
|
8
|
+
export { projectBuildxResult, type DockerBuildResultLike, type ProjectBuildxResultArgs } from "./projectBuildxResult.js";
|
|
9
|
+
export { abortChildProcess } from "./abortHelpers.js";
|
|
10
|
+
export { DockerCli, type DockerCliLogger, type DockerCliOptions, type BuildxProgressEvent, type PushProgressEvent, type PushResult, type PullProgressEvent, type PullResult, type ImagetoolsInspect, type EcrLoginArgs, type BuildxCapabilities, type DaemonInfo } from "./DockerCli.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isSuccess as E,isFailure as _,success as o,failure as i}from"./result.js";import{DOCKER_CLI_LOG_CATEGORY as L,DOCKER_CLI_BUILDX_LOG_CATEGORY as s,BUILDX_VERSION_FLOOR as t,ENGINE_VERSION_FLOOR as D,PrerequisiteMissingExitCode as O,DEFAULT_BUILDER_NAME as I,DEFAULT_DOCKER_BIN as R,SIGTERM_GRACE_MS as S,STDERR_TAIL_LINES as U,DEFAULT_BUILD_TIMEOUT_MS as l,DEFAULT_PUSH_TIMEOUT_MS as x,DEFAULT_PULL_TIMEOUT_MS as M,DEFAULT_INSPECT_TIMEOUT_MS as A,DEFAULT_DAEMON_PROBE_TIMEOUT_MS as C}from"./dockerCliConstants.js";import{BuildxBuildArgsSchema as c,BuildxBuildResultSchema as m,DockerCliErrorKindSchema as u,DockerCliErrorSchema as p,isDockerCliErrorKind as d}from"./dockerCliSchemas.js";import{buildxArgvBuilder as f}from"./buildxArgvBuilder.js";import{parseRawjsonLine as N}from"./rawjsonParser.js";import{rawjsonToVertexEvent as G}from"./rawjsonToVertexEvent.js";import{parseMetadataFile as h}from"./metadataFileParser.js";import{projectBuildxResult as k}from"./projectBuildxResult.js";import{abortChildProcess as j}from"./abortHelpers.js";import{DockerCli as b}from"./DockerCli.js";export{t as BUILDX_VERSION_FLOOR,c as BuildxBuildArgsSchema,m as BuildxBuildResultSchema,I as DEFAULT_BUILDER_NAME,l as DEFAULT_BUILD_TIMEOUT_MS,C as DEFAULT_DAEMON_PROBE_TIMEOUT_MS,R as DEFAULT_DOCKER_BIN,A as DEFAULT_INSPECT_TIMEOUT_MS,M as DEFAULT_PULL_TIMEOUT_MS,x as DEFAULT_PUSH_TIMEOUT_MS,s as DOCKER_CLI_BUILDX_LOG_CATEGORY,L as DOCKER_CLI_LOG_CATEGORY,b as DockerCli,u as DockerCliErrorKindSchema,p as DockerCliErrorSchema,D as ENGINE_VERSION_FLOOR,O as PrerequisiteMissingExitCode,S as SIGTERM_GRACE_MS,U as STDERR_TAIL_LINES,j as abortChildProcess,f as buildxArgvBuilder,i as failure,d as isDockerCliErrorKind,_ as isFailure,E as isSuccess,h as parseMetadataFile,N as parseRawjsonLine,k as projectBuildxResult,G as rawjsonToVertexEvent,o as success};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads + parses the JSON file `docker buildx build` writes via
|
|
3
|
+
* `--metadata-file`. Returns the raw metadata blob; consumer code (e.g.
|
|
4
|
+
* `projectBuildxResult.ts`) is responsible for extracting specific keys
|
|
5
|
+
* such as `containerimage.digest`.
|
|
6
|
+
*
|
|
7
|
+
* Three failure shapes mapped to distinct DockerCliError kinds:
|
|
8
|
+
* - `metadata_missing` — file does not exist (ENOENT)
|
|
9
|
+
* - `metadata_malformed` — file exists but contents are not valid JSON
|
|
10
|
+
* - `inspect_failed` — any other read I/O error
|
|
11
|
+
*
|
|
12
|
+
* Out of scope: writing the metadata file (buildx does that), choosing the
|
|
13
|
+
* tmp path (the DockerCli caller does that), digest extraction (projection).
|
|
14
|
+
*/
|
|
15
|
+
import { type DockerCliError } from "./dockerCliSchemas.js";
|
|
16
|
+
import { type Result } from "./result.js";
|
|
17
|
+
export declare function parseMetadataFile(path: string): Promise<Result<Record<string, unknown>, DockerCliError>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFile as n}from"node:fs/promises";import{maskSensitiveOutput as m}from"../securityHelpers.js";import{makeError as a}from"./DockerCli.js";import{failure as i,success as f}from"./result.js";async function S(e){let o;try{o=await n(e,"utf8")}catch(r){if(r?.code==="ENOENT")return i(a("metadata_missing",`Buildx metadata file not found at ${e}`,{path:e}));const s=m(r instanceof Error?r.message:String(r));return i(a("inspect_failed",`Failed to read buildx metadata file at ${e}: ${s}`,{path:e}))}let t;try{t=JSON.parse(o)}catch(r){const d=m(r instanceof Error?r.message:String(r));return i(a("metadata_malformed",`Buildx metadata file at ${e} is not valid JSON: ${d}`,{path:e}))}return t===null||typeof t!="object"||Array.isArray(t)?i(a("metadata_malformed",`Buildx metadata file at ${e} is not a JSON object`,{path:e})):f(t)}export{S as parseMetadataFile};
|