@flighthq/tool-pipeline 0.5.1-edge.685.f72fc7f

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/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2013-2026 Joshua Granick and other contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # @flighthq/tool-pipeline
2
+
3
+ Server and CI content-addressed pass-through asset publisher with canonical manifests
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @flighthq/tool-pipeline
9
+ ```
10
+
11
+ Import the supported application-facing API from `@flighthq/tool-pipeline`. The `@flighthq/tool-pipeline/contract` export is the wider package-to-package contract used to compose Flight itself.
12
+
13
+ This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
14
+
15
+ - [Flight project](https://github.com/flighthq/flight)
16
+ - [Source for @flighthq/tool-pipeline@0.5.1-edge.685.f72fc7f](https://github.com/flighthq/flight/tree/f72fc7feaa9653d950f39c7d2fe6365f48e82e51/packages/tool-pipeline)
17
+ - [License](https://github.com/flighthq/flight/blob/f72fc7feaa9653d950f39c7d2fe6365f48e82e51/LICENSE.md)
package/dist/bin.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
package/dist/bin.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runToolPipeline } from './pipelineTool';
3
+ process.exitCode = await runToolPipeline(process.argv.slice(2), {
4
+ writeError: (message) => process.stderr.write(message),
5
+ writeOutput: (message) => process.stdout.write(message),
6
+ });
7
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,CAAC,QAAQ,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IAC9D,UAAU,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;IACtD,WAAW,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;CACxD,CAAC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export * from './pipelineBuild';
2
+ export * from './pipelineConfig';
3
+ export * from './pipelineTool';
4
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC"}
@@ -0,0 +1,4 @@
1
+ export * from './pipelineBuild';
2
+ export * from './pipelineConfig';
3
+ export * from './pipelineTool';
4
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './contract';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './contract';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { AssetType } from '@flighthq/types/contract';
2
+ export declare const TOOL_PIPELINE_MANIFEST_SCHEMA_VERSION = 1;
3
+ export interface ToolPipelineBuildOptions {
4
+ readonly configPath: string;
5
+ readonly outputDirectory: string;
6
+ }
7
+ export interface ToolPipelineBuildResult {
8
+ readonly manifest: Readonly<ToolPipelineManifest>;
9
+ readonly manifestHash: string;
10
+ readonly outputFiles: readonly string[];
11
+ }
12
+ export interface ToolPipelineManifest {
13
+ readonly assets: readonly Readonly<ToolPipelineManifestAsset>[];
14
+ readonly schemaVersion: 1;
15
+ }
16
+ export interface ToolPipelineManifestAsset {
17
+ readonly byteLength: number;
18
+ readonly contentHash: string;
19
+ readonly groups?: readonly string[];
20
+ readonly id: string;
21
+ readonly type: AssetType;
22
+ readonly url: string;
23
+ }
24
+ export declare function buildToolPipeline(options: Readonly<ToolPipelineBuildOptions>): Promise<ToolPipelineBuildResult>;
25
+ //# sourceMappingURL=pipelineBuild.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipelineBuild.d.ts","sourceRoot":"","sources":["../src/pipelineBuild.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAK1D,eAAO,MAAM,qCAAqC,IAAI,CAAC;AAEvD,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,oBAAoB,CAAC,CAAC;IAClD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,SAAS,QAAQ,CAAC,yBAAyB,CAAC,EAAE,CAAC;IAChE,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,QAAQ,CAAC,wBAAwB,CAAC,GAAG,OAAO,CAAC,uBAAuB,CAAC,CA6BrH"}
@@ -0,0 +1,91 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { basename, dirname, join, resolve } from 'node:path';
4
+ import { parseToolPipelineConfig } from './pipelineConfig';
5
+ export const TOOL_PIPELINE_MANIFEST_SCHEMA_VERSION = 1;
6
+ export async function buildToolPipeline(options) {
7
+ const configPath = resolve(options.configPath);
8
+ const outputDirectory = resolve(options.outputDirectory);
9
+ if (await pathExists(outputDirectory)) {
10
+ throw new Error(`tool-pipeline: output directory already exists: ${outputDirectory}`);
11
+ }
12
+ const config = parseToolPipelineConfig(await readFile(configPath, 'utf8'), configPath);
13
+ const configDirectory = dirname(configPath);
14
+ const outputByUrl = new Map();
15
+ const manifestAssets = [];
16
+ for (const source of [...config.assets].sort((a, b) => compareCodeUnits(a.id, b.id))) {
17
+ const bytes = await readFile(resolve(configDirectory, ...source.source.split('/')));
18
+ const contentHash = `sha256-${hashBytes(bytes)}`;
19
+ const url = `assets/${contentHash}${getNormalizedExtension(source.source)}`;
20
+ outputByUrl.set(url, bytes);
21
+ manifestAssets.push(createManifestAsset(source, bytes.byteLength, contentHash, url));
22
+ }
23
+ const manifest = {
24
+ assets: manifestAssets,
25
+ schemaVersion: TOOL_PIPELINE_MANIFEST_SCHEMA_VERSION,
26
+ };
27
+ const manifestText = `${JSON.stringify(manifest, null, 2)}\n`;
28
+ const manifestHash = `sha256-${hashBytes(Buffer.from(manifestText, 'utf8'))}`;
29
+ const outputFiles = ['asset-manifest.json', ...outputByUrl.keys()].sort(compareCodeUnits);
30
+ await publishToolPipelineOutput(outputDirectory, outputByUrl, manifestText);
31
+ return { manifest, manifestHash, outputFiles };
32
+ }
33
+ function compareCodeUnits(a, b) {
34
+ return a < b ? -1 : a > b ? 1 : 0;
35
+ }
36
+ function createManifestAsset(source, byteLength, contentHash, url) {
37
+ return {
38
+ byteLength,
39
+ contentHash,
40
+ ...(source.groups === undefined ? {} : { groups: [...source.groups] }),
41
+ id: source.id,
42
+ type: source.type,
43
+ url,
44
+ };
45
+ }
46
+ function getNormalizedExtension(source) {
47
+ const name = source.slice(source.lastIndexOf('/') + 1);
48
+ const extension = /\.([A-Za-z0-9]+)$/.exec(name)?.[1];
49
+ return extension === undefined ? '' : `.${extension.toLowerCase()}`;
50
+ }
51
+ function hashBytes(bytes) {
52
+ return createHash('sha256').update(bytes).digest('hex');
53
+ }
54
+ async function pathExists(path) {
55
+ try {
56
+ await lstat(path);
57
+ return true;
58
+ }
59
+ catch (error) {
60
+ if (isNodeError(error) && error.code === 'ENOENT')
61
+ return false;
62
+ throw error;
63
+ }
64
+ }
65
+ async function publishToolPipelineOutput(outputDirectory, outputByUrl, manifestText) {
66
+ const parent = dirname(outputDirectory);
67
+ await mkdir(parent, { recursive: true });
68
+ const stagingDirectory = await mkdtemp(join(parent, `.${basename(outputDirectory)}.tmp-`));
69
+ let published = false;
70
+ try {
71
+ for (const [url, bytes] of [...outputByUrl.entries()].sort((a, b) => compareCodeUnits(a[0], b[0]))) {
72
+ const path = join(stagingDirectory, ...url.split('/'));
73
+ await mkdir(dirname(path), { recursive: true });
74
+ await writeFile(path, bytes);
75
+ }
76
+ await writeFile(join(stagingDirectory, 'asset-manifest.json'), manifestText, 'utf8');
77
+ if (await pathExists(outputDirectory)) {
78
+ throw new Error(`tool-pipeline: output directory already exists: ${outputDirectory}`);
79
+ }
80
+ await rename(stagingDirectory, outputDirectory);
81
+ published = true;
82
+ }
83
+ finally {
84
+ if (!published)
85
+ await rm(stagingDirectory, { force: true, recursive: true });
86
+ }
87
+ }
88
+ function isNodeError(error) {
89
+ return error instanceof Error && 'code' in error;
90
+ }
91
+ //# sourceMappingURL=pipelineBuild.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipelineBuild.js","sourceRoot":"","sources":["../src/pipelineBuild.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { AssetType } from '@flighthq/types/contract';
2
+ export declare const TOOL_PIPELINE_CONFIG_SCHEMA_VERSION = 1;
3
+ export interface ToolPipelineConfig {
4
+ readonly assets: readonly Readonly<ToolPipelineSource>[];
5
+ readonly schemaVersion: 1;
6
+ }
7
+ export interface ToolPipelineSource {
8
+ readonly groups?: readonly string[];
9
+ readonly id: string;
10
+ readonly source: string;
11
+ readonly type: AssetType;
12
+ }
13
+ export declare function parseToolPipelineConfig(source: string, sourceName?: string): ToolPipelineConfig;
14
+ //# sourceMappingURL=pipelineConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipelineConfig.d.ts","sourceRoot":"","sources":["../src/pipelineConfig.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE1D,eAAO,MAAM,mCAAmC,IAAI,CAAC;AAErD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,SAAS,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;IACzD,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;CAC1B;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,EACd,UAAU,GAAE,MAA+B,GAC1C,kBAAkB,CAiDpB"}
@@ -0,0 +1,73 @@
1
+ export const TOOL_PIPELINE_CONFIG_SCHEMA_VERSION = 1;
2
+ export function parseToolPipelineConfig(source, sourceName = 'tool pipeline config') {
3
+ let value;
4
+ try {
5
+ value = JSON.parse(source);
6
+ }
7
+ catch {
8
+ throw new Error(`${sourceName}: invalid JSON`);
9
+ }
10
+ if (!isRecord(value))
11
+ throw new Error(`${sourceName}: config must be an object`);
12
+ assertKnownFields(value, ['assets', 'schemaVersion'], sourceName);
13
+ if (value['schemaVersion'] !== TOOL_PIPELINE_CONFIG_SCHEMA_VERSION) {
14
+ throw new Error(`${sourceName}: schemaVersion must equal ${TOOL_PIPELINE_CONFIG_SCHEMA_VERSION}`);
15
+ }
16
+ if (!Array.isArray(value['assets']))
17
+ throw new Error(`${sourceName}: assets must be an array`);
18
+ const ids = new Set();
19
+ const assets = [];
20
+ for (let index = 0; index < value['assets'].length; index++) {
21
+ const input = value['assets'][index];
22
+ const label = `${sourceName}: assets[${index}]`;
23
+ if (!isRecord(input))
24
+ throw new Error(`${label} must be an object`);
25
+ assertKnownFields(input, ['groups', 'id', 'source', 'type'], label);
26
+ const id = input['id'];
27
+ const assetSource = input['source'];
28
+ const type = input['type'];
29
+ if (typeof id !== 'string' || id.length === 0)
30
+ throw new Error(`${label}.id must be a non-empty string`);
31
+ if (ids.has(id))
32
+ throw new Error(`${sourceName}: duplicate asset id "${id}"`);
33
+ ids.add(id);
34
+ if (typeof assetSource !== 'string' || !isPortableRelativePath(assetSource)) {
35
+ throw new Error(`${label}.source must be a portable relative path`);
36
+ }
37
+ if (typeof type !== 'string' || type.length === 0)
38
+ throw new Error(`${label}.type must be a non-empty string`);
39
+ const groups = input['groups'];
40
+ if (groups !== undefined &&
41
+ (!Array.isArray(groups) || groups.some((group) => typeof group !== 'string' || group.length === 0))) {
42
+ throw new Error(`${label}.groups must contain non-empty strings`);
43
+ }
44
+ const normalizedGroups = groups === undefined ? [] : [...new Set(groups)].sort(compareCodeUnits);
45
+ assets.push({
46
+ ...(normalizedGroups.length > 0 ? { groups: normalizedGroups } : {}),
47
+ id,
48
+ source: assetSource,
49
+ type: type,
50
+ });
51
+ }
52
+ return { assets, schemaVersion: TOOL_PIPELINE_CONFIG_SCHEMA_VERSION };
53
+ }
54
+ function assertKnownFields(value, fields, label) {
55
+ const known = new Set(fields);
56
+ const unknown = Object.keys(value)
57
+ .filter((field) => !known.has(field))
58
+ .sort(compareCodeUnits)[0];
59
+ if (unknown !== undefined)
60
+ throw new Error(`${label}: unknown field "${unknown}"`);
61
+ }
62
+ function compareCodeUnits(a, b) {
63
+ return a < b ? -1 : a > b ? 1 : 0;
64
+ }
65
+ function isPortableRelativePath(value) {
66
+ if (value.length === 0 || value.startsWith('/') || value.includes('\\') || /^[A-Za-z]:/.test(value))
67
+ return false;
68
+ return value.split('/').every((segment) => segment !== '' && segment !== '.' && segment !== '..');
69
+ }
70
+ function isRecord(value) {
71
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
72
+ }
73
+ //# sourceMappingURL=pipelineConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipelineConfig.js","sourceRoot":"","sources":["../src/pipelineConfig.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export interface ToolPipelineToolIO {
2
+ readonly writeError: (message: string) => void;
3
+ readonly writeOutput: (message: string) => void;
4
+ }
5
+ export declare function runToolPipeline(args: readonly string[], io: Readonly<ToolPipelineToolIO>): Promise<number>;
6
+ //# sourceMappingURL=pipelineTool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipelineTool.d.ts","sourceRoot":"","sources":["../src/pipelineTool.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACjD;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBhH"}
@@ -0,0 +1,42 @@
1
+ import { buildToolPipeline } from './pipelineBuild';
2
+ const USAGE = 'Usage: tool-pipeline build --config <file> --out <new-directory>\n';
3
+ export async function runToolPipeline(args, io) {
4
+ if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
5
+ io.writeOutput(USAGE);
6
+ return 0;
7
+ }
8
+ const options = parseBuildArguments(args);
9
+ if (options === null) {
10
+ io.writeError(USAGE);
11
+ return 1;
12
+ }
13
+ try {
14
+ const result = await buildToolPipeline(options);
15
+ io.writeOutput(`${result.manifestHash} asset-manifest.json\n`);
16
+ return 0;
17
+ }
18
+ catch (error) {
19
+ io.writeError(`${error instanceof Error ? error.message : String(error)}\n`);
20
+ return 1;
21
+ }
22
+ }
23
+ function parseBuildArguments(args) {
24
+ if (args.length !== 5 || args[0] !== 'build')
25
+ return null;
26
+ let configPath = null;
27
+ let outputDirectory = null;
28
+ for (let index = 1; index < args.length; index += 2) {
29
+ const flag = args[index];
30
+ const value = args[index + 1];
31
+ if (value === undefined || value.length === 0)
32
+ return null;
33
+ if (flag === '--config' && configPath === null)
34
+ configPath = value;
35
+ else if (flag === '--out' && outputDirectory === null)
36
+ outputDirectory = value;
37
+ else
38
+ return null;
39
+ }
40
+ return configPath === null || outputDirectory === null ? null : { configPath, outputDirectory };
41
+ }
42
+ //# sourceMappingURL=pipelineTool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipelineTool.js","sourceRoot":"","sources":["../src/pipelineTool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAOpD,MAAM,KAAK,GAAG,oEAAoE,CAAC;AAEnF,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAuB,EAAE,EAAgC;IAC7F,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC;QACpE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QACtB,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,OAAO,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACrB,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAChD,EAAE,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC,YAAY,yBAAyB,CAAC,CAAC;QAChE,OAAO,CAAC,CAAC;IACX,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,EAAE,CAAC,UAAU,CAAC,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC7E,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC;AAED,SAAS,mBAAmB,CAC1B,IAAuB;IAEvB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1D,IAAI,UAAU,GAAkB,IAAI,CAAC;IACrC,IAAI,eAAe,GAAkB,IAAI,CAAC;IAC1C,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QAC9B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAC3D,IAAI,IAAI,KAAK,UAAU,IAAI,UAAU,KAAK,IAAI;YAAE,UAAU,GAAG,KAAK,CAAC;aAC9D,IAAI,IAAI,KAAK,OAAO,IAAI,eAAe,KAAK,IAAI;YAAE,eAAe,GAAG,KAAK,CAAC;;YAC1E,OAAO,IAAI,CAAC;IACnB,CAAC;IACD,OAAO,UAAU,KAAK,IAAI,IAAI,eAAe,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC;AAClG,CAAC"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@flighthq/tool-pipeline",
3
+ "version": "0.5.1-edge.685.f72fc7f",
4
+ "author": "Joshua Granick and other contributors",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/flighthq/flight.git",
9
+ "directory": "packages/tool-pipeline"
10
+ },
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ },
19
+ "./contract": {
20
+ "types": "./dist/contract.d.ts",
21
+ "default": "./dist/contract.js"
22
+ }
23
+ },
24
+ "bin": {
25
+ "tool-pipeline": "dist/bin.js"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "src/**/*.test.ts",
30
+ "!dist/**/*.test.js",
31
+ "!dist/**/*.test.d.ts",
32
+ "!dist/**/*.test.js.map",
33
+ "!dist/**/*.test.d.ts.map"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc -b",
37
+ "clean": "tsc -b --clean",
38
+ "test": "vitest run --config vitest.config.ts",
39
+ "test:watch": "vitest --watch --config vitest.config.ts",
40
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
41
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
42
+ },
43
+ "dependencies": {
44
+ "@flighthq/types": "0.5.1-edge.685.f72fc7f"
45
+ },
46
+ "devDependencies": {
47
+ "typescript": "^5.3.0"
48
+ },
49
+ "description": "Server and CI content-addressed pass-through asset publisher with canonical manifests",
50
+ "sideEffects": false
51
+ }
@@ -0,0 +1,187 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { access, mkdir, mkdtemp, readFile, rm, utimes, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+
6
+ import { buildToolPipeline } from './pipelineBuild';
7
+
8
+ const roots: string[] = [];
9
+
10
+ afterEach(async () => {
11
+ await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true })));
12
+ });
13
+
14
+ describe('buildToolPipeline', () => {
15
+ it('publishes exact source bytes at their full SHA-256 URL with a canonical manifest', async () => {
16
+ const root = await createRoot();
17
+ const bytes = new Uint8Array([0, 1, 2, 255]);
18
+ const configPath = await writeConfig(root, {
19
+ assets: [{ groups: ['boot'], id: 'hero', source: 'images/hero.PNG', type: 'image' }],
20
+ schemaVersion: 1,
21
+ });
22
+ await writeBytes(join(root, 'images', 'hero.PNG'), bytes);
23
+ const outputDirectory = join(root, 'out');
24
+
25
+ const result = await buildToolPipeline({ configPath, outputDirectory });
26
+
27
+ const contentHash = `sha256-${sha256(bytes)}`;
28
+ const assetUrl = `assets/${contentHash}.png`;
29
+ const expectedManifest = {
30
+ assets: [{ byteLength: 4, contentHash, groups: ['boot'], id: 'hero', type: 'image', url: assetUrl }],
31
+ schemaVersion: 1,
32
+ };
33
+ const expectedText = `${JSON.stringify(expectedManifest, null, 2)}\n`;
34
+ expect(result).toEqual({
35
+ manifest: expectedManifest,
36
+ manifestHash: `sha256-${sha256(expectedText)}`,
37
+ outputFiles: ['asset-manifest.json', assetUrl],
38
+ });
39
+ expect(await readFile(join(outputDirectory, assetUrl))).toEqual(Buffer.from(bytes));
40
+ expect(await readFile(join(outputDirectory, 'asset-manifest.json'), 'utf8')).toBe(expectedText);
41
+ });
42
+
43
+ it('is byte-identical across config order, group order, roots, and mtimes with explicit code-unit sorting', async () => {
44
+ const first = await createDeterminismFixture(false, new Date(1_000));
45
+ const second = await createDeterminismFixture(true, new Date(9_000_000));
46
+
47
+ const firstResult = await buildToolPipeline(first);
48
+ const secondResult = await buildToolPipeline(second);
49
+ expect(firstResult).toEqual(secondResult);
50
+ expect(firstResult.manifest.assets.map((asset) => asset.id)).toEqual(['Z', 'a', 'ä']);
51
+ expect(firstResult.manifest.assets.find((asset) => asset.id === 'a')?.groups).toEqual(['alpha', 'zeta']);
52
+ expect(await readFile(join(first.outputDirectory, 'asset-manifest.json'))).toEqual(
53
+ await readFile(join(second.outputDirectory, 'asset-manifest.json')),
54
+ );
55
+ for (const file of firstResult.outputFiles.filter((path) => path !== 'asset-manifest.json')) {
56
+ expect(await readFile(join(first.outputDirectory, file))).toEqual(
57
+ await readFile(join(second.outputDirectory, file)),
58
+ );
59
+ }
60
+ });
61
+
62
+ it('changes only the affected artifact identity and manifest hash when one source byte changes', async () => {
63
+ const first = await createChangeFixture(2);
64
+ const second = await createChangeFixture(3);
65
+ const firstResult = await buildToolPipeline(first);
66
+ const secondResult = await buildToolPipeline(second);
67
+ const firstStable = firstResult.manifest.assets.find((asset) => asset.id === 'stable');
68
+ const secondStable = secondResult.manifest.assets.find((asset) => asset.id === 'stable');
69
+ const firstChanged = firstResult.manifest.assets.find((asset) => asset.id === 'changed');
70
+ const secondChanged = secondResult.manifest.assets.find((asset) => asset.id === 'changed');
71
+
72
+ expect(firstStable).toEqual(secondStable);
73
+ expect(firstChanged?.url).not.toBe(secondChanged?.url);
74
+ expect(firstChanged?.contentHash).not.toBe(secondChanged?.contentHash);
75
+ expect(firstResult.manifestHash).not.toBe(secondResult.manifestHash);
76
+ });
77
+
78
+ it('deduplicates equal bytes with equal normalized extensions while retaining both manifest IDs', async () => {
79
+ const root = await createRoot();
80
+ const bytes = new Uint8Array([4, 5, 6]);
81
+ const configPath = await writeConfig(root, {
82
+ assets: [
83
+ { id: 'first', source: 'first.png', type: 'image' },
84
+ { id: 'second', source: 'second.PNG', type: 'image' },
85
+ ],
86
+ schemaVersion: 1,
87
+ });
88
+ await writeBytes(join(root, 'first.png'), bytes);
89
+ await writeBytes(join(root, 'second.PNG'), bytes);
90
+
91
+ const result = await buildToolPipeline({ configPath, outputDirectory: join(root, 'out') });
92
+
93
+ expect(result.manifest.assets).toHaveLength(2);
94
+ expect(result.manifest.assets[0]?.url).toBe(result.manifest.assets[1]?.url);
95
+ expect(result.outputFiles).toHaveLength(2);
96
+ });
97
+
98
+ it('does not publish an output directory when a later source cannot be read', async () => {
99
+ const root = await createRoot();
100
+ const configPath = await writeConfig(root, {
101
+ assets: [
102
+ { id: 'first', source: 'first.bin', type: 'data' },
103
+ { id: 'missing', source: 'missing.bin', type: 'data' },
104
+ ],
105
+ schemaVersion: 1,
106
+ });
107
+ await writeBytes(join(root, 'first.bin'), new Uint8Array([1]));
108
+ const outputDirectory = join(root, 'out');
109
+
110
+ await expect(buildToolPipeline({ configPath, outputDirectory })).rejects.toThrow('missing.bin');
111
+ await expect(access(outputDirectory)).rejects.toThrow();
112
+ });
113
+
114
+ it('rejects an existing output directory without changing it', async () => {
115
+ const root = await createRoot();
116
+ const configPath = await writeConfig(root, {
117
+ assets: [{ id: 'asset', source: 'asset.bin', type: 'data' }],
118
+ schemaVersion: 1,
119
+ });
120
+ await writeBytes(join(root, 'asset.bin'), new Uint8Array([1]));
121
+ const outputDirectory = join(root, 'out');
122
+ await mkdir(outputDirectory);
123
+ await writeFile(join(outputDirectory, 'sentinel.txt'), 'keep');
124
+
125
+ await expect(buildToolPipeline({ configPath, outputDirectory })).rejects.toThrow('output directory already exists');
126
+ expect(await readFile(join(outputDirectory, 'sentinel.txt'), 'utf8')).toBe('keep');
127
+ await expect(access(join(outputDirectory, 'asset-manifest.json'))).rejects.toThrow();
128
+ });
129
+ });
130
+
131
+ async function createChangeFixture(changedByte: number): Promise<{ configPath: string; outputDirectory: string }> {
132
+ const root = await createRoot();
133
+ const configPath = await writeConfig(root, {
134
+ assets: [
135
+ { id: 'stable', source: 'stable.bin', type: 'data' },
136
+ { id: 'changed', source: 'changed.bin', type: 'data' },
137
+ ],
138
+ schemaVersion: 1,
139
+ });
140
+ await writeBytes(join(root, 'stable.bin'), new Uint8Array([1]));
141
+ await writeBytes(join(root, 'changed.bin'), new Uint8Array([changedByte]));
142
+ return { configPath, outputDirectory: join(root, 'out') };
143
+ }
144
+
145
+ async function createDeterminismFixture(
146
+ reverse: boolean,
147
+ mtime: Date,
148
+ ): Promise<{ configPath: string; outputDirectory: string }> {
149
+ const root = await createRoot();
150
+ const assets = [
151
+ { id: 'ä', source: 'images/umlaut.PNG', type: 'image' },
152
+ { groups: reverse ? ['alpha', 'zeta'] : ['zeta', 'alpha', 'zeta'], id: 'a', source: 'data/a.bin', type: 'data' },
153
+ { id: 'Z', source: 'data/z.bin', type: 'data' },
154
+ ];
155
+ const configPath = await writeConfig(root, { assets: reverse ? [...assets].reverse() : assets, schemaVersion: 1 });
156
+ for (const [source, bytes] of [
157
+ ['images/umlaut.PNG', new Uint8Array([9, 8, 7])],
158
+ ['data/a.bin', new Uint8Array([1, 2])],
159
+ ['data/z.bin', new Uint8Array([3, 4])],
160
+ ] as const) {
161
+ const path = join(root, ...source.split('/'));
162
+ await writeBytes(path, bytes);
163
+ await utimes(path, mtime, mtime);
164
+ }
165
+ return { configPath, outputDirectory: join(root, 'out') };
166
+ }
167
+
168
+ async function createRoot(): Promise<string> {
169
+ const root = await mkdtemp(join(tmpdir(), 'flight-tool-pipeline-'));
170
+ roots.push(root);
171
+ return root;
172
+ }
173
+
174
+ function sha256(value: string | Readonly<Uint8Array>): string {
175
+ return createHash('sha256').update(value).digest('hex');
176
+ }
177
+
178
+ async function writeBytes(path: string, bytes: Readonly<Uint8Array>): Promise<void> {
179
+ await mkdir(dirname(path), { recursive: true });
180
+ await writeFile(path, bytes);
181
+ }
182
+
183
+ async function writeConfig(root: string, config: unknown): Promise<string> {
184
+ const path = join(root, 'tool-pipeline.json');
185
+ await writeFile(path, JSON.stringify(config));
186
+ return path;
187
+ }
@@ -0,0 +1,81 @@
1
+ import { parseToolPipelineConfig } from './pipelineConfig';
2
+
3
+ describe('parseToolPipelineConfig', () => {
4
+ it('accepts the strict version-1 shape and normalizes groups without reordering assets', () => {
5
+ const config = parseToolPipelineConfig(
6
+ JSON.stringify({
7
+ assets: [
8
+ { groups: ['z', 'a', 'z'], id: 'second', source: 'images/hero.PNG', type: 'image' },
9
+ { id: 'first', source: 'data/scene.bin', type: 'scene' },
10
+ ],
11
+ schemaVersion: 1,
12
+ }),
13
+ 'fixture.json',
14
+ );
15
+
16
+ expect(config).toEqual({
17
+ assets: [
18
+ { groups: ['a', 'z'], id: 'second', source: 'images/hero.PNG', type: 'image' },
19
+ { id: 'first', source: 'data/scene.bin', type: 'scene' },
20
+ ],
21
+ schemaVersion: 1,
22
+ });
23
+ });
24
+
25
+ it('rejects missing or unsupported schema versions and unknown fields', () => {
26
+ expect(() => parseToolPipelineConfig('{"assets":[]}', 'missing.json')).toThrow(
27
+ 'missing.json: schemaVersion must equal 1',
28
+ );
29
+ expect(() => parseToolPipelineConfig('{"assets":[],"schemaVersion":2}', 'newer.json')).toThrow(
30
+ 'newer.json: schemaVersion must equal 1',
31
+ );
32
+ expect(() => parseToolPipelineConfig('{"assets":[],"schemaVersion":1,"watch":true}', 'unknown.json')).toThrow(
33
+ 'unknown.json: unknown field "watch"',
34
+ );
35
+ expect(() =>
36
+ parseToolPipelineConfig(
37
+ '{"assets":[{"id":"a","source":"a.bin","transform":"copy","type":"data"}],"schemaVersion":1}',
38
+ 'asset-unknown.json',
39
+ ),
40
+ ).toThrow('asset-unknown.json: assets[0]: unknown field "transform"');
41
+ });
42
+
43
+ it.each(['/absolute.png', '../escape.png', './local.png', 'nested//file.png', 'nested\\file.png', 'C:/file.png'])(
44
+ 'rejects non-portable source path %s',
45
+ (source) => {
46
+ expect(() =>
47
+ parseToolPipelineConfig(
48
+ JSON.stringify({ assets: [{ id: 'asset', source, type: 'image' }], schemaVersion: 1 }),
49
+ 'paths.json',
50
+ ),
51
+ ).toThrow('paths.json: assets[0].source must be a portable relative path');
52
+ },
53
+ );
54
+
55
+ it('rejects duplicate IDs and empty scalar/group values', () => {
56
+ expect(() =>
57
+ parseToolPipelineConfig(
58
+ JSON.stringify({
59
+ assets: [
60
+ { id: 'same', source: 'a.bin', type: 'data' },
61
+ { id: 'same', source: 'b.bin', type: 'data' },
62
+ ],
63
+ schemaVersion: 1,
64
+ }),
65
+ 'duplicates.json',
66
+ ),
67
+ ).toThrow('duplicates.json: duplicate asset id "same"');
68
+ expect(() =>
69
+ parseToolPipelineConfig(
70
+ JSON.stringify({ assets: [{ groups: [''], id: 'asset', source: 'a.bin', type: 'data' }], schemaVersion: 1 }),
71
+ 'groups.json',
72
+ ),
73
+ ).toThrow('groups.json: assets[0].groups must contain non-empty strings');
74
+ expect(() =>
75
+ parseToolPipelineConfig(
76
+ JSON.stringify({ assets: [{ id: '', source: 'a.bin', type: 'data' }], schemaVersion: 1 }),
77
+ 'id.json',
78
+ ),
79
+ ).toThrow('id.json: assets[0].id must be a non-empty string');
80
+ });
81
+ });
@@ -0,0 +1,79 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ import { runToolPipeline } from './pipelineTool';
7
+
8
+ const roots: string[] = [];
9
+
10
+ afterEach(async () => {
11
+ await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true })));
12
+ });
13
+
14
+ describe('runToolPipeline', () => {
15
+ it('prints help successfully and rejects unknown commands at the thin CLI boundary', async () => {
16
+ await expect(run(['--help'])).resolves.toEqual({
17
+ errors: [],
18
+ exitCode: 0,
19
+ output: ['Usage: tool-pipeline build --config <file> --out <new-directory>\n'],
20
+ });
21
+ await expect(run(['transform'])).resolves.toEqual({
22
+ errors: ['Usage: tool-pipeline build --config <file> --out <new-directory>\n'],
23
+ exitCode: 1,
24
+ output: [],
25
+ });
26
+ });
27
+
28
+ it('builds from explicit paths and prints only the deterministic manifest identity', async () => {
29
+ const root = await createRoot();
30
+ const configPath = join(root, 'tool-pipeline.json');
31
+ const outputDirectory = join(root, 'out');
32
+ await writeFile(join(root, 'asset.bin'), new Uint8Array([1, 2, 3]));
33
+ await writeFile(
34
+ configPath,
35
+ JSON.stringify({ assets: [{ id: 'asset', source: 'asset.bin', type: 'data' }], schemaVersion: 1 }),
36
+ );
37
+
38
+ const result = await run(['build', '--config', configPath, '--out', outputDirectory]);
39
+ const manifest = await readFile(join(outputDirectory, 'asset-manifest.json'));
40
+
41
+ expect(result).toEqual({
42
+ errors: [],
43
+ exitCode: 0,
44
+ output: [`sha256-${createHash('sha256').update(manifest).digest('hex')} asset-manifest.json\n`],
45
+ });
46
+ });
47
+
48
+ it('reports config/build failures without publishing a manifest or printing a stack', async () => {
49
+ const root = await createRoot();
50
+ const configPath = join(root, 'tool-pipeline.json');
51
+ const outputDirectory = join(root, 'out');
52
+ await writeFile(configPath, '{"assets":[],"schemaVersion":2}');
53
+
54
+ const result = await run(['build', '--config', configPath, '--out', outputDirectory]);
55
+
56
+ expect(result.exitCode).toBe(1);
57
+ expect(result.output).toEqual([]);
58
+ expect(result.errors.join('')).toContain('schemaVersion must equal 1');
59
+ expect(result.errors.join('')).not.toContain('\n at ');
60
+ await expect(readFile(join(outputDirectory, 'asset-manifest.json'))).rejects.toThrow();
61
+ });
62
+ });
63
+
64
+ async function createRoot(): Promise<string> {
65
+ const root = await mkdtemp(join(tmpdir(), 'flight-tool-pipeline-cli-'));
66
+ roots.push(root);
67
+ await mkdir(root, { recursive: true });
68
+ return root;
69
+ }
70
+
71
+ async function run(args: readonly string[]): Promise<{ errors: string[]; exitCode: number; output: string[] }> {
72
+ const errors: string[] = [];
73
+ const output: string[] = [];
74
+ const exitCode = await runToolPipeline(args, {
75
+ writeError: (message) => errors.push(message),
76
+ writeOutput: (message) => output.push(message),
77
+ });
78
+ return { errors, exitCode, output };
79
+ }