@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.
Files changed (76) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/Config.d.ts +1 -1
  3. package/dist/appPath.d.ts +29 -0
  4. package/dist/appPath.js +1 -0
  5. package/dist/constructMap.d.ts +9 -13
  6. package/dist/constructMap.js +1 -1
  7. package/dist/deriveContentHashTag.d.ts +34 -0
  8. package/dist/deriveContentHashTag.js +1 -0
  9. package/dist/docker/DockerCli.build.d.ts +42 -0
  10. package/dist/docker/DockerCli.build.js +1 -0
  11. package/dist/docker/DockerCli.d.ts +135 -0
  12. package/dist/docker/DockerCli.daemon.d.ts +24 -0
  13. package/dist/docker/DockerCli.daemon.js +1 -0
  14. package/dist/docker/DockerCli.js +1 -0
  15. package/dist/docker/DockerCli.registry.d.ts +19 -0
  16. package/dist/docker/DockerCli.registry.js +3 -0
  17. package/dist/docker/abortHelpers.d.ts +18 -0
  18. package/dist/docker/abortHelpers.js +1 -0
  19. package/dist/docker/buildxArgvBuilder.d.ts +15 -0
  20. package/dist/docker/buildxArgvBuilder.js +1 -0
  21. package/dist/docker/dockerCliConstants.d.ts +15 -0
  22. package/dist/docker/dockerCliConstants.js +1 -0
  23. package/dist/docker/dockerCliSchemas.d.ts +88 -0
  24. package/dist/docker/dockerCliSchemas.js +1 -0
  25. package/dist/docker/index.d.ts +10 -0
  26. package/dist/docker/index.js +1 -0
  27. package/dist/docker/metadataFileParser.d.ts +17 -0
  28. package/dist/docker/metadataFileParser.js +1 -0
  29. package/dist/docker/projectBuildxResult.d.ts +35 -0
  30. package/dist/docker/projectBuildxResult.js +1 -0
  31. package/dist/docker/rawjsonParser.d.ts +56 -0
  32. package/dist/docker/rawjsonParser.js +1 -0
  33. package/dist/docker/rawjsonToVertexEvent.d.ts +64 -0
  34. package/dist/docker/rawjsonToVertexEvent.js +1 -0
  35. package/dist/docker/result.d.ts +34 -0
  36. package/dist/docker/result.js +1 -0
  37. package/dist/errorUtils.d.ts +14 -4
  38. package/dist/findInfrastructurePaths.d.ts +62 -0
  39. package/dist/findInfrastructurePaths.js +1 -0
  40. package/dist/findRepoRoot.d.ts +12 -0
  41. package/dist/findRepoRoot.js +1 -0
  42. package/dist/fsHelpers.d.ts +15 -0
  43. package/dist/fsHelpers.js +1 -1
  44. package/dist/fsScan.d.ts +15 -0
  45. package/dist/fsScan.js +1 -0
  46. package/dist/index.d.ts +8 -0
  47. package/dist/index.js +1 -1
  48. package/dist/inferContainerFromCandidates.d.ts +23 -0
  49. package/dist/inferContainerFromCandidates.js +1 -0
  50. package/dist/manifest/index.d.ts +10 -0
  51. package/dist/manifest/index.js +1 -0
  52. package/dist/manifest/io.d.ts +60 -0
  53. package/dist/manifest/io.js +1 -0
  54. package/dist/manifest/schemas.d.ts +163 -0
  55. package/dist/manifest/schemas.js +1 -0
  56. package/dist/mcpProtocol/index.d.ts +362 -0
  57. package/dist/mcpProtocol/index.js +1 -0
  58. package/dist/migration/clickhouseSqlUsers.d.ts +72 -0
  59. package/dist/migration/clickhouseSqlUsers.js +1 -0
  60. package/dist/migration/constants.d.ts +34 -0
  61. package/dist/migration/constants.js +1 -0
  62. package/dist/migration/index.d.ts +3 -0
  63. package/dist/migration/index.js +1 -0
  64. package/dist/migration/pickLatestPrismaMigration.d.ts +10 -0
  65. package/dist/migration/pickLatestPrismaMigration.js +1 -0
  66. package/dist/reservedAppNames.d.ts +30 -0
  67. package/dist/reservedAppNames.js +1 -0
  68. package/dist/scanLocalRepository.d.ts +24 -0
  69. package/dist/scanLocalRepository.js +1 -0
  70. package/dist/scanTypes.d.ts +17 -0
  71. package/dist/scanTypes.js +0 -0
  72. package/dist/securityHelpers.d.ts +11 -1
  73. package/dist/securityHelpers.js +1 -1
  74. package/dist/tokenScopes.d.ts +11 -0
  75. package/dist/tokenScopes.js +1 -0
  76. package/package.json +43 -9
package/dist/.minified CHANGED
@@ -1 +1 @@
1
- 21 files minified at 2026-04-27T00:08:06.074Z
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;
@@ -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};
@@ -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
- /** Manifest file name — single source of truth across CLI, deploy-core, and infrastructure. */
12
- export declare const FJALL_MANIFEST_FILENAME = "fjall-manifest.json";
13
- /** Current manifest schema version. Shared between CLI and infrastructure. */
14
- export declare const MANIFEST_SCHEMA_VERSION: 1;
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).
@@ -1 +1 @@
1
- const R="fjall-manifest.json",S=1;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 h=Object.freeze({CloudTrail:"monitoring",MonitoringRole:"monitoring",AuditRole:"security",OidcConnector:"security",EcrDefaultImage:"registry",EventBus:"events",DisasterRecovery:"backup"}),A=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 E(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 i of c){if(i.type!=="aws:cdk:logicalId")continue;const u=i.data,f=s.get(u)??"",m=C(a,e)??d(f);t.set(u,{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 v(o){const e={};for(const[t,r]of o)e[t]=r;return e}function x(o){const e=new Map;if(!o)return e;for(const[t,r]of Object.entries(o))e.set(t,r);return e}function I(o,e,t){const r=t?.get(o);return r?{group:r.group,constructPath:r.constructPath,displayName:T(r.constructPath)}:{displayName:y(e)}}export{h as ACCOUNT_CONSTRUCT_GROUPS,A as APP_CONSTRUCT_GROUPS,R as FJALL_MANIFEST_FILENAME,S as MANIFEST_SCHEMA_VERSION,E as buildConstructMap,v as constructMapToRecord,T as deriveDisplayName,C as deriveGroupFromPath,I as enrichFromConstructMap,x as recordToConstructMap};
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};