@habitat-ai/resource-source-inventory 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Effect } from "effect";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
/** Maximum root-path length admitted by one source-inventory observation. */
|
|
4
|
+
export declare const MAX_SOURCE_INVENTORY_ROOT_LENGTH = 16384;
|
|
5
|
+
/** Maximum repository-relative path length admitted by the resource contract. */
|
|
6
|
+
export declare const MAX_SOURCE_INVENTORY_PATH_LENGTH = 4096;
|
|
7
|
+
/** Maximum number of unique visible entry paths admitted by one observation. */
|
|
8
|
+
export declare const MAX_SOURCE_INVENTORY_ENTRIES = 100000;
|
|
9
|
+
/** Maximum diagnostic detail exposed by one source-inventory failure. */
|
|
10
|
+
export declare const MAX_SOURCE_INVENTORY_FAILURE_DETAIL = 4096;
|
|
11
|
+
/** Schema for one canonical, safe, repository-relative visible entry path. */
|
|
12
|
+
export declare const SourceInventoryPathSchema: Type.TRefine<Type.TString>;
|
|
13
|
+
/** Structural schema for one bounded source-inventory observation request. */
|
|
14
|
+
export declare const ObserveSourceInventoryInputSchema: Type.TObject<{
|
|
15
|
+
root: Type.TReadonly<Type.TString>;
|
|
16
|
+
maxEntries: Type.TReadonly<Type.TInteger>;
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* Schema for one canonical visible-entry inventory and its tracked non-file
|
|
20
|
+
* subset.
|
|
21
|
+
*/
|
|
22
|
+
export declare const SourceInventoryResultSchema: Type.TRefine<Type.TObject<{
|
|
23
|
+
paths: Type.TReadonly<Type.TImmutable<Type.TArray<Type.TRefine<Type.TString>>>>;
|
|
24
|
+
trackedNonFilePaths: Type.TReadonly<Type.TImmutable<Type.TArray<Type.TRefine<Type.TString>>>>;
|
|
25
|
+
}>>;
|
|
26
|
+
/** Provider-neutral mechanical reasons an inventory observation can fail. */
|
|
27
|
+
export declare const SourceInventoryFailureReasonSchema: Type.TUnion<[Type.TLiteral<"InvalidInput">, Type.TLiteral<"SetupFailed">, Type.TLiteral<"LimitExceeded">, Type.TLiteral<"CommandFailed">, Type.TLiteral<"InvalidOutput">]>;
|
|
28
|
+
/** Structural schema for one bounded typed source-inventory failure. */
|
|
29
|
+
export declare const SourceInventoryFailureSchema: Type.TObject<{
|
|
30
|
+
_tag: Type.TReadonly<Type.TLiteral<"SourceInventoryFailure">>;
|
|
31
|
+
reason: Type.TReadonly<Type.TUnion<[Type.TLiteral<"InvalidInput">, Type.TLiteral<"SetupFailed">, Type.TLiteral<"LimitExceeded">, Type.TLiteral<"CommandFailed">, Type.TLiteral<"InvalidOutput">]>>;
|
|
32
|
+
path: Type.TReadonly<Type.TOptional<Type.TString>>;
|
|
33
|
+
detail: Type.TReadonly<Type.TString>;
|
|
34
|
+
}>;
|
|
35
|
+
/** One canonical repository-relative visible entry path. */
|
|
36
|
+
export type SourceInventoryPath = Static<typeof SourceInventoryPathSchema>;
|
|
37
|
+
/** Input for one bounded visible-entry inventory observation. */
|
|
38
|
+
export type ObserveSourceInventoryInput = Static<typeof ObserveSourceInventoryInputSchema>;
|
|
39
|
+
/** Canonical visible entry paths and the tracked non-file subset. */
|
|
40
|
+
export type SourceInventoryResult = Static<typeof SourceInventoryResultSchema>;
|
|
41
|
+
/** One mechanical source-inventory failure reason. */
|
|
42
|
+
export type SourceInventoryFailureReason = Static<typeof SourceInventoryFailureReasonSchema>;
|
|
43
|
+
/** One bounded typed source-inventory failure. */
|
|
44
|
+
export type SourceInventoryFailure = Static<typeof SourceInventoryFailureSchema>;
|
|
45
|
+
/** Checks an unknown value against the complete source-inventory failure contract. */
|
|
46
|
+
export declare function isSourceInventoryFailure(input: unknown): input is SourceInventoryFailure;
|
|
47
|
+
/** Provider-neutral capability for observing one local visible-entry inventory. */
|
|
48
|
+
export interface SourceInventoryResource<R = never> {
|
|
49
|
+
readonly observe: (input: ObserveSourceInventoryInput) => Effect.Effect<SourceInventoryResult, SourceInventoryFailure, R>;
|
|
50
|
+
}
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { ReadonlyObject, Refine, Type } from "typebox";
|
|
2
|
+
import { Validator } from "typebox/schema";
|
|
3
|
+
/** Maximum root-path length admitted by one source-inventory observation. */
|
|
4
|
+
export const MAX_SOURCE_INVENTORY_ROOT_LENGTH = 16_384;
|
|
5
|
+
/** Maximum repository-relative path length admitted by the resource contract. */
|
|
6
|
+
export const MAX_SOURCE_INVENTORY_PATH_LENGTH = 4_096;
|
|
7
|
+
/** Maximum number of unique visible entry paths admitted by one observation. */
|
|
8
|
+
export const MAX_SOURCE_INVENTORY_ENTRIES = 100_000;
|
|
9
|
+
/** Maximum diagnostic detail exposed by one source-inventory failure. */
|
|
10
|
+
export const MAX_SOURCE_INVENTORY_FAILURE_DETAIL = 4_096;
|
|
11
|
+
const SourceInventoryRootSchema = Type.String({
|
|
12
|
+
minLength: 1,
|
|
13
|
+
maxLength: MAX_SOURCE_INVENTORY_ROOT_LENGTH,
|
|
14
|
+
description: "Caller-selected local source root",
|
|
15
|
+
});
|
|
16
|
+
/** Schema for one canonical, safe, repository-relative visible entry path. */
|
|
17
|
+
export const SourceInventoryPathSchema = Refine(Type.String({
|
|
18
|
+
minLength: 1,
|
|
19
|
+
maxLength: MAX_SOURCE_INVENTORY_PATH_LENGTH,
|
|
20
|
+
description: "Canonical repository-relative visible entry path",
|
|
21
|
+
}), isCanonicalRelativePath, () => "Expected a canonical safe repository-relative path");
|
|
22
|
+
const MaxEntriesSchema = Type.Integer({
|
|
23
|
+
minimum: 1,
|
|
24
|
+
maximum: MAX_SOURCE_INVENTORY_ENTRIES,
|
|
25
|
+
description: "Maximum unique visible entry paths the observation may return",
|
|
26
|
+
});
|
|
27
|
+
/** Structural schema for one bounded source-inventory observation request. */
|
|
28
|
+
export const ObserveSourceInventoryInputSchema = ReadonlyObject(Type.Object({
|
|
29
|
+
root: SourceInventoryRootSchema,
|
|
30
|
+
maxEntries: MaxEntriesSchema,
|
|
31
|
+
}), { additionalProperties: false });
|
|
32
|
+
const SourceInventoryPathsSchema = ReadonlyObject(Type.Array(SourceInventoryPathSchema), {
|
|
33
|
+
maxItems: MAX_SOURCE_INVENTORY_ENTRIES,
|
|
34
|
+
description: "Unique Git-visible entry paths in canonical order",
|
|
35
|
+
});
|
|
36
|
+
const SourceInventoryResultStructureSchema = ReadonlyObject(Type.Object({
|
|
37
|
+
paths: SourceInventoryPathsSchema,
|
|
38
|
+
trackedNonFilePaths: ReadonlyObject(Type.Array(SourceInventoryPathSchema), {
|
|
39
|
+
maxItems: MAX_SOURCE_INVENTORY_ENTRIES,
|
|
40
|
+
description: "Tracked symlink and Gitlink entry paths in canonical order",
|
|
41
|
+
}),
|
|
42
|
+
}), { additionalProperties: false });
|
|
43
|
+
/**
|
|
44
|
+
* Schema for one canonical visible-entry inventory and its tracked non-file
|
|
45
|
+
* subset.
|
|
46
|
+
*/
|
|
47
|
+
export const SourceInventoryResultSchema = Refine(SourceInventoryResultStructureSchema, isCanonicalInventory, () => "Expected sorted unique paths and a tracked non-file subset");
|
|
48
|
+
/** Provider-neutral mechanical reasons an inventory observation can fail. */
|
|
49
|
+
export const SourceInventoryFailureReasonSchema = Type.Union([
|
|
50
|
+
Type.Literal("InvalidInput"),
|
|
51
|
+
Type.Literal("SetupFailed"),
|
|
52
|
+
Type.Literal("LimitExceeded"),
|
|
53
|
+
Type.Literal("CommandFailed"),
|
|
54
|
+
Type.Literal("InvalidOutput"),
|
|
55
|
+
], {
|
|
56
|
+
description: "Mechanical reason a source inventory could not be observed",
|
|
57
|
+
});
|
|
58
|
+
/** Structural schema for one bounded typed source-inventory failure. */
|
|
59
|
+
export const SourceInventoryFailureSchema = ReadonlyObject(Type.Object({
|
|
60
|
+
_tag: Type.Literal("SourceInventoryFailure", {
|
|
61
|
+
description: "Discriminator for one source-inventory failure",
|
|
62
|
+
}),
|
|
63
|
+
reason: SourceInventoryFailureReasonSchema,
|
|
64
|
+
path: Type.Optional(Type.String({
|
|
65
|
+
minLength: 1,
|
|
66
|
+
maxLength: MAX_SOURCE_INVENTORY_ROOT_LENGTH,
|
|
67
|
+
description: "Local source root associated with the failed operation",
|
|
68
|
+
})),
|
|
69
|
+
detail: Type.String({
|
|
70
|
+
minLength: 1,
|
|
71
|
+
maxLength: MAX_SOURCE_INVENTORY_FAILURE_DETAIL,
|
|
72
|
+
description: "Bounded operational failure detail",
|
|
73
|
+
}),
|
|
74
|
+
}), { additionalProperties: false });
|
|
75
|
+
const sourceInventoryFailureValidator = new Validator({}, SourceInventoryFailureSchema);
|
|
76
|
+
/** Checks an unknown value against the complete source-inventory failure contract. */
|
|
77
|
+
export function isSourceInventoryFailure(input) {
|
|
78
|
+
return sourceInventoryFailureValidator.Check(input);
|
|
79
|
+
}
|
|
80
|
+
function isCanonicalRelativePath(value) {
|
|
81
|
+
return (!value.startsWith("/") &&
|
|
82
|
+
!value.endsWith("/") &&
|
|
83
|
+
!value.includes("\\") &&
|
|
84
|
+
!/^[A-Za-z]:\//u.test(value) &&
|
|
85
|
+
!/[\u0000-\u001f\u007f]/u.test(value) &&
|
|
86
|
+
value.split("/").every((segment) => segment !== "" && segment !== "." && segment !== ".."));
|
|
87
|
+
}
|
|
88
|
+
function isCanonicalInventory(value) {
|
|
89
|
+
if (!isStrictlySorted(value.paths) || !isStrictlySorted(value.trackedNonFilePaths)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const paths = new Set(value.paths);
|
|
93
|
+
return value.trackedNonFilePaths.every((path) => paths.has(path));
|
|
94
|
+
}
|
|
95
|
+
function isStrictlySorted(paths) {
|
|
96
|
+
for (let index = 1; index < paths.length; index += 1) {
|
|
97
|
+
const previous = paths[index - 1];
|
|
98
|
+
const current = paths[index];
|
|
99
|
+
if (previous === undefined || current === undefined || compareText(previous, current) >= 0) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
function compareText(left, right) {
|
|
106
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
107
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SourceInventoryResource } from "@habitat-ai/resource-source-inventory";
|
|
2
|
+
import { ChildProcessSpawner } from "effect/unstable/process";
|
|
3
|
+
import { type Static, Type } from "typebox";
|
|
4
|
+
type ProviderRequirements = ChildProcessSpawner.ChildProcessSpawner;
|
|
5
|
+
/** Structural schema for local-Git source-inventory provider construction. */
|
|
6
|
+
export declare const GitSourceInventoryProviderOptionsSchema: Type.TObject<{
|
|
7
|
+
gitExecutable: Type.TReadonly<Type.TOptional<Type.TString>>;
|
|
8
|
+
}>;
|
|
9
|
+
/**
|
|
10
|
+
* Provider options reserved for focused tests. Production resolution uses the
|
|
11
|
+
* operator's ordinary `git` command and inherited process configuration.
|
|
12
|
+
*/
|
|
13
|
+
export type GitSourceInventoryProviderOptions = Static<typeof GitSourceInventoryProviderOptionsSchema>;
|
|
14
|
+
/** Creates a local-Git source inventory over Effect child-process services. */
|
|
15
|
+
export declare function makeGitSourceInventoryResource(options?: GitSourceInventoryProviderOptions): SourceInventoryResource<ProviderRequirements>;
|
|
16
|
+
/** Creates a ready Node realization of the local-Git source inventory provider. */
|
|
17
|
+
export declare function makeNodeGitSourceInventoryResource(options?: GitSourceInventoryProviderOptions): SourceInventoryResource<never>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { NodeServices } from "@effect/platform-node";
|
|
2
|
+
import { MAX_SOURCE_INVENTORY_FAILURE_DETAIL, MAX_SOURCE_INVENTORY_ROOT_LENGTH, ObserveSourceInventoryInputSchema, SourceInventoryPathSchema, SourceInventoryResultSchema, } from "@habitat-ai/resource-source-inventory";
|
|
3
|
+
import { Effect, Stream } from "effect";
|
|
4
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
5
|
+
import { ReadonlyObject, Type } from "typebox";
|
|
6
|
+
import { Validator } from "typebox/schema";
|
|
7
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
8
|
+
const GIT_INVENTORY_ARGUMENTS = Object.freeze([
|
|
9
|
+
"ls-files",
|
|
10
|
+
"--cached",
|
|
11
|
+
"--others",
|
|
12
|
+
"--exclude-standard",
|
|
13
|
+
"--stage",
|
|
14
|
+
"--abbrev=1",
|
|
15
|
+
"-t",
|
|
16
|
+
"-z",
|
|
17
|
+
]);
|
|
18
|
+
const PROCESS_FORCE_KILL_AFTER = "5 seconds";
|
|
19
|
+
const STDERR_LIMIT = 64 * 1_024;
|
|
20
|
+
const STDOUT_LIMIT = 64 * 1_024 * 1_024;
|
|
21
|
+
const TRACKED_RECORD_PATTERN = /^[HSMRCK] (?<mode>100644|100755|120000|160000) [0-9a-f]{1,64} [0-3]\t(?<path>.+)$/su;
|
|
22
|
+
const observeInputValidator = new Validator({}, ObserveSourceInventoryInputSchema);
|
|
23
|
+
const sourcePathValidator = new Validator({}, SourceInventoryPathSchema);
|
|
24
|
+
const sourceInventoryResultValidator = new Validator({}, SourceInventoryResultSchema);
|
|
25
|
+
/** Structural schema for local-Git source-inventory provider construction. */
|
|
26
|
+
export const GitSourceInventoryProviderOptionsSchema = ReadonlyObject(Type.Object({
|
|
27
|
+
gitExecutable: Type.Optional(Type.String({
|
|
28
|
+
minLength: 1,
|
|
29
|
+
maxLength: MAX_SOURCE_INVENTORY_ROOT_LENGTH,
|
|
30
|
+
pattern: "^[^\\u0000]+$",
|
|
31
|
+
description: "Focused-test override for the ordinary Git executable",
|
|
32
|
+
})),
|
|
33
|
+
}), { additionalProperties: false });
|
|
34
|
+
const providerOptionsValidator = new Validator({}, GitSourceInventoryProviderOptionsSchema);
|
|
35
|
+
/** Creates a local-Git source inventory over Effect child-process services. */
|
|
36
|
+
export function makeGitSourceInventoryResource(options = {}) {
|
|
37
|
+
const observe = Effect.fn("sourceInventory.git.observe")(function* (input) {
|
|
38
|
+
if (!providerOptionsValidator.Check(options)) {
|
|
39
|
+
return yield* fail("InvalidInput", undefined, "Source-inventory provider options do not match their structural contract");
|
|
40
|
+
}
|
|
41
|
+
if (!observeInputValidator.Check(input)) {
|
|
42
|
+
return yield* fail("InvalidInput", undefined, "Source-inventory input does not match the bounded resource contract");
|
|
43
|
+
}
|
|
44
|
+
const gitExecutable = options.gitExecutable ?? "git";
|
|
45
|
+
const command = yield* runGitInventory(gitExecutable, input.root);
|
|
46
|
+
if (command.exitCode !== 0) {
|
|
47
|
+
return yield* fail("CommandFailed", input.root, gitFailureDetail(command.exitCode, command.stderr));
|
|
48
|
+
}
|
|
49
|
+
return yield* parseGitInventory(command.stdout, input.root, input.maxEntries);
|
|
50
|
+
});
|
|
51
|
+
return Object.freeze({ observe });
|
|
52
|
+
}
|
|
53
|
+
/** Creates a ready Node realization of the local-Git source inventory provider. */
|
|
54
|
+
export function makeNodeGitSourceInventoryResource(options = {}) {
|
|
55
|
+
const resource = makeGitSourceInventoryResource(options);
|
|
56
|
+
return Object.freeze({
|
|
57
|
+
observe: (input) => resource.observe(input).pipe(Effect.provide(NodeServices.layer)),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function runGitInventory(executable, root) {
|
|
61
|
+
const command = ChildProcess.make(executable, GIT_INVENTORY_ARGUMENTS, {
|
|
62
|
+
cwd: root,
|
|
63
|
+
extendEnv: true,
|
|
64
|
+
stdin: "ignore",
|
|
65
|
+
forceKillAfter: PROCESS_FORCE_KILL_AFTER,
|
|
66
|
+
});
|
|
67
|
+
return Effect.scoped(Effect.gen(function* () {
|
|
68
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
69
|
+
const process = yield* spawner.spawn(command).pipe(mapPlatform(root, "SetupFailed"));
|
|
70
|
+
const [stdout, stderr, exitCode] = yield* Effect.all([
|
|
71
|
+
collectBoundedOutput(process.stdout, STDOUT_LIMIT, "stdout", root),
|
|
72
|
+
collectBoundedOutput(process.stderr, STDERR_LIMIT, "stderr", root),
|
|
73
|
+
process.exitCode.pipe(mapPlatform(root, "CommandFailed")),
|
|
74
|
+
], { concurrency: "unbounded" });
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
stdout,
|
|
77
|
+
stderr,
|
|
78
|
+
exitCode: Number(exitCode),
|
|
79
|
+
});
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
function collectBoundedOutput(stream, maxBytes, output, root) {
|
|
83
|
+
const initial = Object.freeze({
|
|
84
|
+
chunks: Object.freeze([]),
|
|
85
|
+
bytes: 0,
|
|
86
|
+
});
|
|
87
|
+
return Stream.runFoldEffect(stream.pipe(Stream.mapError((cause) => platformFailure(root, cause, "CommandFailed"))), () => initial, (state, chunk) => {
|
|
88
|
+
const bytes = state.bytes + chunk.byteLength;
|
|
89
|
+
if (bytes > maxBytes) {
|
|
90
|
+
return fail(output === "stdout" ? "LimitExceeded" : "CommandFailed", root, `Git ${output} exceeds the ${maxBytes}-byte source-inventory bound`);
|
|
91
|
+
}
|
|
92
|
+
return Effect.succeed(Object.freeze({
|
|
93
|
+
chunks: Object.freeze([...state.chunks, chunk]),
|
|
94
|
+
bytes,
|
|
95
|
+
}));
|
|
96
|
+
}).pipe(Effect.map((state) => concatenate(state.chunks, state.bytes)));
|
|
97
|
+
}
|
|
98
|
+
function parseGitInventory(bytes, root, maxEntries) {
|
|
99
|
+
return decodeGitOutput(bytes, root).pipe(Effect.flatMap((text) => {
|
|
100
|
+
if (text.length === 0) {
|
|
101
|
+
return checkedResult(Object.freeze({ paths: Object.freeze([]), trackedNonFilePaths: Object.freeze([]) }), root);
|
|
102
|
+
}
|
|
103
|
+
if (!text.endsWith("\0")) {
|
|
104
|
+
return fail("InvalidOutput", root, "Git inventory output has a truncated NUL record");
|
|
105
|
+
}
|
|
106
|
+
const facts = new Map();
|
|
107
|
+
for (const rawRecord of text.slice(0, -1).split("\0")) {
|
|
108
|
+
if (rawRecord.length === 0) {
|
|
109
|
+
return fail("InvalidOutput", root, "Git inventory output contains an empty NUL record");
|
|
110
|
+
}
|
|
111
|
+
const record = parseGitRecord(rawRecord);
|
|
112
|
+
if (record === undefined) {
|
|
113
|
+
return fail("InvalidOutput", root, "Git ls-files emitted a malformed source-inventory record");
|
|
114
|
+
}
|
|
115
|
+
if (!sourcePathValidator.Check(record.path)) {
|
|
116
|
+
return fail("InvalidOutput", root, "Git ls-files emitted an unsafe or non-canonical relative path");
|
|
117
|
+
}
|
|
118
|
+
const existing = facts.get(record.path);
|
|
119
|
+
if (existing !== undefined) {
|
|
120
|
+
if (existing.source !== record.source) {
|
|
121
|
+
return fail("InvalidOutput", root, "Git ls-files reported one path as both tracked and untracked");
|
|
122
|
+
}
|
|
123
|
+
if (existing.source === "tracked" && record.source === "tracked") {
|
|
124
|
+
facts.set(record.path, Object.freeze({
|
|
125
|
+
source: "tracked",
|
|
126
|
+
path: record.path,
|
|
127
|
+
trackedNonFile: existing.trackedNonFile || record.trackedNonFile,
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
facts.set(record.path, record);
|
|
133
|
+
if (facts.size > maxEntries) {
|
|
134
|
+
return fail("LimitExceeded", root, `Git-visible source inventory exceeds maxEntries ${maxEntries}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const paths = Array.from(facts.keys()).sort(compareText);
|
|
138
|
+
const trackedNonFilePaths = Array.from(facts.values())
|
|
139
|
+
.flatMap((record) => record.source === "tracked" && record.trackedNonFile ? [record.path] : [])
|
|
140
|
+
.sort(compareText);
|
|
141
|
+
return checkedResult(Object.freeze({
|
|
142
|
+
paths: Object.freeze(paths),
|
|
143
|
+
trackedNonFilePaths: Object.freeze(trackedNonFilePaths),
|
|
144
|
+
}), root);
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
function parseGitRecord(record) {
|
|
148
|
+
if (record.startsWith("? ")) {
|
|
149
|
+
const path = normalizeUntrackedPath(record.slice(2));
|
|
150
|
+
return Object.freeze({ source: "untracked", path });
|
|
151
|
+
}
|
|
152
|
+
const match = TRACKED_RECORD_PATTERN.exec(record);
|
|
153
|
+
const mode = match?.groups?.mode;
|
|
154
|
+
const path = match?.groups?.path;
|
|
155
|
+
if (mode === undefined || path === undefined)
|
|
156
|
+
return undefined;
|
|
157
|
+
return Object.freeze({
|
|
158
|
+
source: "tracked",
|
|
159
|
+
path,
|
|
160
|
+
trackedNonFile: mode === "120000" || mode === "160000",
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function normalizeUntrackedPath(path) {
|
|
164
|
+
return path.endsWith("/") && !path.endsWith("//") ? path.slice(0, -1) : path;
|
|
165
|
+
}
|
|
166
|
+
function checkedResult(result, root) {
|
|
167
|
+
return sourceInventoryResultValidator.Check(result)
|
|
168
|
+
? Effect.succeed(result)
|
|
169
|
+
: fail("InvalidOutput", root, "Git inventory could not be normalized to the source-inventory contract");
|
|
170
|
+
}
|
|
171
|
+
function decodeGitOutput(bytes, root) {
|
|
172
|
+
return Effect.try({
|
|
173
|
+
try: () => decoder.decode(bytes),
|
|
174
|
+
catch: () => failure("InvalidOutput", root, "Git returned non-UTF-8 source-inventory output"),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function gitFailureDetail(exitCode, stderr) {
|
|
178
|
+
let detail;
|
|
179
|
+
try {
|
|
180
|
+
detail = decoder.decode(stderr).trim();
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
detail = "non-UTF-8 stderr";
|
|
184
|
+
}
|
|
185
|
+
return detail.length === 0
|
|
186
|
+
? `Git ls-files exited ${exitCode}`
|
|
187
|
+
: `Git ls-files exited ${exitCode}: ${detail}`;
|
|
188
|
+
}
|
|
189
|
+
function concatenate(chunks, length) {
|
|
190
|
+
const bytes = new Uint8Array(length);
|
|
191
|
+
let offset = 0;
|
|
192
|
+
for (const chunk of chunks) {
|
|
193
|
+
bytes.set(chunk, offset);
|
|
194
|
+
offset += chunk.byteLength;
|
|
195
|
+
}
|
|
196
|
+
return bytes;
|
|
197
|
+
}
|
|
198
|
+
function mapPlatform(root, reason) {
|
|
199
|
+
return (effect) => effect.pipe(Effect.mapError((cause) => platformFailure(root, cause, reason)));
|
|
200
|
+
}
|
|
201
|
+
function platformFailure(root, cause, reason) {
|
|
202
|
+
return failure(reason, root, cause.message);
|
|
203
|
+
}
|
|
204
|
+
function fail(reason, root, detail) {
|
|
205
|
+
return Effect.fail(failure(reason, root, detail));
|
|
206
|
+
}
|
|
207
|
+
function failure(reason, root, detail) {
|
|
208
|
+
const normalized = detail.trim() || "Source-inventory observation failed";
|
|
209
|
+
const boundedDetail = normalized.length <= MAX_SOURCE_INVENTORY_FAILURE_DETAIL
|
|
210
|
+
? normalized
|
|
211
|
+
: `${normalized.slice(0, MAX_SOURCE_INVENTORY_FAILURE_DETAIL - 3)}...`;
|
|
212
|
+
return Object.freeze({
|
|
213
|
+
_tag: "SourceInventoryFailure",
|
|
214
|
+
reason,
|
|
215
|
+
...(root === undefined || root.length === 0 ? {} : { path: root }),
|
|
216
|
+
detail: boundedDetail,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function compareText(left, right) {
|
|
220
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
221
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@habitat-ai/resource-source-inventory",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"main": "./dist/contract.js",
|
|
10
|
+
"types": "./dist/contract.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/contract.d.ts",
|
|
14
|
+
"default": "./dist/contract.js"
|
|
15
|
+
},
|
|
16
|
+
"./providers/git-effect-platform-node": {
|
|
17
|
+
"types": "./dist/providers/git-effect-platform-node/index.d.ts",
|
|
18
|
+
"default": "./dist/providers/git-effect-platform-node/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.build.json",
|
|
23
|
+
"test": "bun test test/contract.test.ts",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@effect/platform-node": "4.0.0-beta.101",
|
|
28
|
+
"effect": "4.0.0-beta.101",
|
|
29
|
+
"typebox": "1.3.8"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"typescript": "^5.9.3"
|
|
33
|
+
},
|
|
34
|
+
"nx": {
|
|
35
|
+
"tags": [
|
|
36
|
+
"type:resource",
|
|
37
|
+
"layer:resource-contract",
|
|
38
|
+
"resource:source-inventory"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|