@telorun/fs 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ # SUSTAINABLE USE LICENSE (Fair-code)
2
+
3
+ Copyright (c) 2026 CodeNet Sp. z o.o.
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 use, copy, modify, and distribute the Software for any purpose—including commercial purposes—subject to the following conditions:
6
+
7
+ 1. ANTI-COMPETITION RESTRICTION: The Software may not be provided to third parties as a managed service, commercial SaaS (Software-as-a-Service), PaaS (Platform-as-a-Service), BaaS (Backend-as-a-Service), or similar offering where the primary value provided to the user is the functionality of the Software itself, without a separate commercial license from the copyright holder.
8
+
9
+ 2. PERMITTED COMMERCIAL USE: You are free to use the Software to build, host, and monetize your own commercial applications, products, and services, provided such use does not violate Clause 1.
10
+
11
+ 3. ATTRIBUTION: This copyright notice and license must be included in all copies or substantial portions of the Software.
12
+
13
+ 4. CONTRIBUTIONS: Contributions to the Software are welcome and encouraged. By contributing, you agree that your contributions may be incorporated into the Software and distributed under this license.
14
+
15
+ 5. DISCLAIMER: 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.
16
+
17
+ For commercial licensing, managed hosting exemptions, or enterprise inquiries, please contact <contact@codenet.pl>.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/telorun/telo/main/assets/telo.png" alt="Telo" width="200" />
3
+ </p>
4
+
5
+ <h1 align="center">Telo</h1>
6
+
7
+ <p align="center">Runtime for declarative backends.</p>
8
+
9
+ <p align="center">
10
+ <a href="https://github.com/telorun/telo/actions/workflows/test.yml"><img alt="Tests" src="https://github.com/telorun/telo/actions/workflows/test.yml/badge.svg" /></a>
11
+ <a href="https://www.npmjs.com/package/@telorun/cli"><img alt="node" src="https://img.shields.io/node/v/@telorun/cli" /></a>
12
+ <br />
13
+ <a href="https://github.com/telorun/telo/commits/main"><img alt="Last commit" src="https://img.shields.io/github/last-commit/telorun/telo" /></a>
14
+ <a href="https://github.com/telorun/telo/issues"><img alt="Issues" src="https://img.shields.io/github/issues/telorun/telo" /></a>
15
+ <a href="https://github.com/telorun/telo/pulls"><img alt="Pull requests" src="https://img.shields.io/github/issues-pr/telorun/telo" /></a>
16
+ <br />
17
+ <img alt="Changesets" src="https://img.shields.io/badge/maintained%20with-changesets-176de3" />
18
+ </p>
19
+
20
+ Telo is an execution engine (Micro-Kernel) that runs logic defined entirely in YAML manifests. Instead of writing imperative backend code, you define your routes, databases, schemas, and AI workflows as atomic, interconnected YAML documents. Telo takes those manifests and runs them.
21
+
22
+ Built to be language-agnostic and infinitely extensible.
23
+
24
+ ```bash
25
+ # Reconcile your manifest into a running backend
26
+ $ telo ./examples/hello-api
27
+
28
+ {"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:8844"}
29
+ ```
30
+
31
+ ## Why use Telo?
32
+
33
+ - **Open Standards:** Built on YAML, JSON Schema, and CEL — no proprietary DSL.
34
+ - **Static Analysis:** CEL type checking, reference validation, and IDE diagnostics catch errors before runtime.
35
+ - **Micro-Kernel Architecture:** Telo itself knows nothing about HTTP or SQL. Everything is a module you import, scope, and compose with typed variable and secret contracts.
36
+ - **Language Agnostic:** Available as a Node.js runtime today, with a shared YAML runtime contract that allows for future Rust or Go implementations without changing your manifests.
37
+
38
+ ## What It Does
39
+
40
+ - **Loads** YAML resources and compiles CEL expressions (`${{ }}`) into an in-memory registry.
41
+ - **Resolves** resource dependencies via a multi-pass init loop, handling ordering automatically.
42
+ - **Indexes** resources by Kind and Name for constant-time lookup.
43
+ - **Dispatches** execution to the controller that owns each Kind.
44
+
45
+ ## Example manifest
46
+
47
+ See [examples/](./examples/) for a list of working applications.
48
+
49
+ ## Status
50
+
51
+ Telo is under **active development**. The core runtime, module system, and standard library are functional, but the API surface — including YAML shapes — may change without notice. Not yet recommended for production use.
52
+
53
+ ## The Meaning of Telo
54
+
55
+ The name Telo is derived from the Greek root Telos - meaning the "end goal", "purpose", or "final state". That is exactly the philosophy behind this runtime. In standard imperative programming, you have to write thousands of lines of code to tell a server exactly how to start. With Telo, you simply declare your desired final state.
56
+
57
+ You define the end state. Telo makes it real.
58
+
59
+ ## Philosophy
60
+
61
+ Modern platforms often spend disproportionate effort on technical mechanics-wiring frameworks, managing infrastructure, and negotiating toolchains-while the original business problem gets delayed or diluted. Telo pushes in the opposite direction: it treats kernel execution as a stable, predictable host so teams can concentrate on the **business logic and outcomes** instead of the plumbing.
62
+
63
+ By separating "what the system should do" from "how it is hosted", the runtime reduces friction for domain‑level changes. Teams can move faster on product requirements, experiment more safely, and keep conversations centered on value delivered rather than implementation trivia.
64
+
65
+ Telo also aims to **join forces across all programming language communities**, so the best ideas, patterns, and implementations can converge into a shared kernel truth without forcing everyone into a single stack.
66
+
67
+ YAML also makes the system more **AI‑friendly** than traditional programming languages: it is explicit, structured, and easier for tools to generate, review, and transform without losing intent.
68
+
69
+ ## Modularity
70
+
71
+ Telo is built around **modules** that own specific resource kinds. A module is loaded from a manifest, declares which kinds it implements, and then receives only the resources of those kinds. This keeps concerns isolated and lets teams compose systems from focused building blocks rather than monolithic services.
72
+
73
+ At kernel execution time, execution is always routed by **Kind.Name**. The kernel resolves the Kind to its owning module and hands off execution. Modules can call back into the kernel to execute other resources, enabling composition without tight coupling.
74
+
75
+ ## Architecture
76
+
77
+ The architecture is inspired by Kubernetes-style manifests: declarative resources, explicit kinds, and a control plane that routes work based on those definitions.
78
+ Those manifests were taken to the next level by allowing them to run inside a standalone runtime host.
79
+
80
+ ## See more at
81
+
82
+ - [Telo Kernel](./kernel/README.md)
83
+ - [Telo SDK for module authors](sdk/README.md)
84
+ - [Modules](modules/README.md)
85
+
86
+ ## License
87
+
88
+ See [LICENSE](https://github.com/telorun/telo/blob/main/LICENSE).
89
+
90
+ ## Contribution Note
91
+
92
+ By contributing, you agree that code and examples in this repository may be translated or re‑implemented in other programming languages (including by AI systems) to support the project’s polyglot goals.
@@ -0,0 +1,17 @@
1
+ import type { ResourceInstance } from "@telorun/sdk";
2
+ import { FsManifest } from "./fs-support.js";
3
+ interface DirectoryCreationInput {
4
+ path: string;
5
+ createParents?: boolean;
6
+ }
7
+ interface DirectoryCreationResult {
8
+ created: boolean;
9
+ }
10
+ declare class DirectoryCreationResource implements ResourceInstance<DirectoryCreationInput, DirectoryCreationResult> {
11
+ private readonly base;
12
+ constructor(base: string);
13
+ invoke(input: DirectoryCreationInput): Promise<DirectoryCreationResult>;
14
+ }
15
+ export declare function register(): void;
16
+ export declare function create(resource: FsManifest): Promise<DirectoryCreationResource>;
17
+ export {};
@@ -0,0 +1,28 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { requirePath, resolveBase, resolveTarget, wrapFsError } from "./fs-support.js";
3
+ class DirectoryCreationResource {
4
+ base;
5
+ constructor(base) {
6
+ this.base = base;
7
+ }
8
+ async invoke(input) {
9
+ const target = resolveTarget(this.base, requirePath("Fs.DirectoryCreation", input?.path));
10
+ try {
11
+ if (input?.createParents) {
12
+ // Recursive mkdir returns the first created path, or undefined when the
13
+ // directory already existed — that's the created/no-op signal.
14
+ const first = await mkdir(target, { recursive: true });
15
+ return { created: first !== undefined };
16
+ }
17
+ await mkdir(target);
18
+ return { created: true };
19
+ }
20
+ catch (err) {
21
+ throw wrapFsError("Fs.DirectoryCreation: cannot create", target, err);
22
+ }
23
+ }
24
+ }
25
+ export function register() { }
26
+ export async function create(resource) {
27
+ return new DirectoryCreationResource(resolveBase(resource.cwd));
28
+ }
@@ -0,0 +1,24 @@
1
+ import type { ResourceInstance } from "@telorun/sdk";
2
+ import { FsManifest } from "./fs-support.js";
3
+ interface DirectoryListingInput {
4
+ path?: string;
5
+ recursive?: boolean;
6
+ }
7
+ interface Entry {
8
+ name: string;
9
+ path: string;
10
+ type: "file" | "directory" | "other";
11
+ size: number;
12
+ }
13
+ interface DirectoryListingResult {
14
+ entries: Entry[];
15
+ }
16
+ declare class DirectoryListingResource implements ResourceInstance<DirectoryListingInput, DirectoryListingResult> {
17
+ private readonly base;
18
+ constructor(base: string);
19
+ invoke(input: DirectoryListingInput): Promise<DirectoryListingResult>;
20
+ private walk;
21
+ }
22
+ export declare function register(): void;
23
+ export declare function create(resource: FsManifest): Promise<DirectoryListingResource>;
24
+ export {};
@@ -0,0 +1,53 @@
1
+ import { lstat, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { resolveBase, resolveTarget, wrapFsError } from "./fs-support.js";
4
+ function classify(stats) {
5
+ if (stats.isFile())
6
+ return "file";
7
+ if (stats.isDirectory())
8
+ return "directory";
9
+ return "other";
10
+ }
11
+ class DirectoryListingResource {
12
+ base;
13
+ constructor(base) {
14
+ this.base = base;
15
+ }
16
+ async invoke(input) {
17
+ const root = input?.path ? resolveTarget(this.base, input.path) : this.base;
18
+ const entries = [];
19
+ await this.walk(root, Boolean(input?.recursive), entries);
20
+ return { entries };
21
+ }
22
+ async walk(dir, recursive, out) {
23
+ let names;
24
+ try {
25
+ names = await readdir(dir);
26
+ }
27
+ catch (err) {
28
+ throw wrapFsError("Fs.DirectoryListing: cannot list", dir, err);
29
+ }
30
+ names.sort();
31
+ for (const name of names) {
32
+ const full = path.join(dir, name);
33
+ // lstat (not stat) so a broken or out-of-tree symlink is reported as
34
+ // "other" rather than throwing. Wrapped so a mid-walk race (entry removed
35
+ // after readdir) or permission error names the path like every other op.
36
+ let stats;
37
+ try {
38
+ stats = await lstat(full);
39
+ }
40
+ catch (err) {
41
+ throw wrapFsError("Fs.DirectoryListing: cannot stat", full, err);
42
+ }
43
+ const type = classify(stats);
44
+ out.push({ name, path: path.relative(this.base, full), type, size: stats.size });
45
+ if (recursive && type === "directory")
46
+ await this.walk(full, recursive, out);
47
+ }
48
+ }
49
+ }
50
+ export function register() { }
51
+ export async function create(resource) {
52
+ return new DirectoryListingResource(resolveBase(resource.cwd));
53
+ }
@@ -0,0 +1,18 @@
1
+ import type { ResourceInstance } from "@telorun/sdk";
2
+ import { FsManifest } from "./fs-support.js";
3
+ interface FileInput {
4
+ path: string;
5
+ encoding?: "utf8" | "base64";
6
+ }
7
+ interface FileResult {
8
+ content: string;
9
+ size: number;
10
+ }
11
+ declare class FileResource implements ResourceInstance<FileInput, FileResult> {
12
+ private readonly base;
13
+ constructor(base: string);
14
+ invoke(input: FileInput): Promise<FileResult>;
15
+ }
16
+ export declare function register(): void;
17
+ export declare function create(resource: FsManifest): Promise<FileResource>;
18
+ export {};
@@ -0,0 +1,23 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { requirePath, resolveBase, resolveTarget, wrapFsError } from "./fs-support.js";
3
+ class FileResource {
4
+ base;
5
+ constructor(base) {
6
+ this.base = base;
7
+ }
8
+ async invoke(input) {
9
+ const target = resolveTarget(this.base, requirePath("Fs.File", input?.path));
10
+ try {
11
+ const buffer = await readFile(target);
12
+ const content = input.encoding === "base64" ? buffer.toString("base64") : buffer.toString("utf8");
13
+ return { content, size: buffer.byteLength };
14
+ }
15
+ catch (err) {
16
+ throw wrapFsError("Fs.File: cannot read", target, err);
17
+ }
18
+ }
19
+ }
20
+ export function register() { }
21
+ export async function create(resource) {
22
+ return new FileResource(resolveBase(resource.cwd));
23
+ }
@@ -0,0 +1,19 @@
1
+ import type { ResourceInstance } from "@telorun/sdk";
2
+ import { FsManifest } from "./fs-support.js";
3
+ interface FileEditInput {
4
+ path: string;
5
+ oldString: string;
6
+ newString: string;
7
+ replaceAll?: boolean;
8
+ }
9
+ interface FileEditResult {
10
+ replacements: number;
11
+ }
12
+ declare class FileEditResource implements ResourceInstance<FileEditInput, FileEditResult> {
13
+ private readonly base;
14
+ constructor(base: string);
15
+ invoke(input: FileEditInput): Promise<FileEditResult>;
16
+ }
17
+ export declare function register(): void;
18
+ export declare function create(resource: FsManifest): Promise<FileEditResource>;
19
+ export {};
@@ -0,0 +1,65 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { requirePath, resolveBase, resolveTarget, wrapFsError } from "./fs-support.js";
3
+ /** Byte offsets of every non-overlapping occurrence of `needle` in `haystack`. */
4
+ function findAll(haystack, needle) {
5
+ const positions = [];
6
+ for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + needle.length)) {
7
+ positions.push(at);
8
+ }
9
+ return positions;
10
+ }
11
+ class FileEditResource {
12
+ base;
13
+ constructor(base) {
14
+ this.base = base;
15
+ }
16
+ async invoke(input) {
17
+ const target = resolveTarget(this.base, requirePath("Fs.FileEdit", input?.path));
18
+ if (typeof input?.oldString !== "string" || input.oldString.length === 0) {
19
+ throw new Error("Fs.FileEdit: 'oldString' input is required and must be a non-empty string");
20
+ }
21
+ if (typeof input?.newString !== "string") {
22
+ throw new Error("Fs.FileEdit: 'newString' input is required and must be a string");
23
+ }
24
+ let buffer;
25
+ try {
26
+ buffer = await readFile(target);
27
+ }
28
+ catch (err) {
29
+ throw wrapFsError("Fs.FileEdit: cannot read", target, err);
30
+ }
31
+ // Operate on bytes, not decoded text: regions outside the match survive
32
+ // byte-for-byte (comments, !cel tags, and any non-UTF-8 content), and the
33
+ // literal search sidesteps regex / `$`-substitution.
34
+ const oldBytes = Buffer.from(input.oldString, "utf8");
35
+ const positions = findAll(buffer, oldBytes);
36
+ const count = positions.length;
37
+ if (count === 0) {
38
+ throw new Error(`Fs.FileEdit: oldString not found in '${target}'`);
39
+ }
40
+ if (count > 1 && !input.replaceAll) {
41
+ throw new Error(`Fs.FileEdit: oldString matches ${count} times in '${target}'; set 'replaceAll: true' or use a more specific string`);
42
+ }
43
+ const newBytes = Buffer.from(input.newString, "utf8");
44
+ const replacements = input.replaceAll ? count : 1;
45
+ const parts = [];
46
+ let cursor = 0;
47
+ for (let i = 0; i < replacements; i++) {
48
+ const at = positions[i];
49
+ parts.push(buffer.subarray(cursor, at), newBytes);
50
+ cursor = at + oldBytes.length;
51
+ }
52
+ parts.push(buffer.subarray(cursor));
53
+ try {
54
+ await writeFile(target, Buffer.concat(parts));
55
+ }
56
+ catch (err) {
57
+ throw wrapFsError("Fs.FileEdit: cannot write", target, err);
58
+ }
59
+ return { replacements };
60
+ }
61
+ }
62
+ export function register() { }
63
+ export async function create(resource) {
64
+ return new FileEditResource(resolveBase(resource.cwd));
65
+ }
@@ -0,0 +1,17 @@
1
+ import type { ResourceInstance } from "@telorun/sdk";
2
+ import { FsManifest } from "./fs-support.js";
3
+ interface FileRemovalInput {
4
+ path: string;
5
+ recursive?: boolean;
6
+ }
7
+ interface FileRemovalResult {
8
+ removed: boolean;
9
+ }
10
+ declare class FileRemovalResource implements ResourceInstance<FileRemovalInput, FileRemovalResult> {
11
+ private readonly base;
12
+ constructor(base: string);
13
+ invoke(input: FileRemovalInput): Promise<FileRemovalResult>;
14
+ }
15
+ export declare function register(): void;
16
+ export declare function create(resource: FsManifest): Promise<FileRemovalResource>;
17
+ export {};
@@ -0,0 +1,23 @@
1
+ import { rm } from "node:fs/promises";
2
+ import { requirePath, resolveBase, resolveTarget, wrapFsError } from "./fs-support.js";
3
+ class FileRemovalResource {
4
+ base;
5
+ constructor(base) {
6
+ this.base = base;
7
+ }
8
+ async invoke(input) {
9
+ const target = resolveTarget(this.base, requirePath("Fs.FileRemoval", input?.path));
10
+ try {
11
+ // force:false so a missing path is surfaced (ENOENT), not swallowed.
12
+ await rm(target, { recursive: Boolean(input?.recursive), force: false });
13
+ return { removed: true };
14
+ }
15
+ catch (err) {
16
+ throw wrapFsError("Fs.FileRemoval: cannot remove", target, err);
17
+ }
18
+ }
19
+ }
20
+ export function register() { }
21
+ export async function create(resource) {
22
+ return new FileRemovalResource(resolveBase(resource.cwd));
23
+ }
@@ -0,0 +1,19 @@
1
+ import type { ResourceInstance } from "@telorun/sdk";
2
+ import { FsManifest } from "./fs-support.js";
3
+ interface FileWriteInput {
4
+ path: string;
5
+ content: string;
6
+ encoding?: "utf8" | "base64";
7
+ createParents?: boolean;
8
+ }
9
+ interface FileWriteResult {
10
+ bytesWritten: number;
11
+ }
12
+ declare class FileWriteResource implements ResourceInstance<FileWriteInput, FileWriteResult> {
13
+ private readonly base;
14
+ constructor(base: string);
15
+ invoke(input: FileWriteInput): Promise<FileWriteResult>;
16
+ }
17
+ export declare function register(): void;
18
+ export declare function create(resource: FsManifest): Promise<FileWriteResource>;
19
+ export {};
@@ -0,0 +1,29 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { requirePath, resolveBase, resolveTarget, wrapFsError } from "./fs-support.js";
4
+ class FileWriteResource {
5
+ base;
6
+ constructor(base) {
7
+ this.base = base;
8
+ }
9
+ async invoke(input) {
10
+ const target = resolveTarget(this.base, requirePath("Fs.FileWrite", input?.path));
11
+ if (typeof input?.content !== "string") {
12
+ throw new Error("Fs.FileWrite: 'content' input is required and must be a string");
13
+ }
14
+ const buffer = Buffer.from(input.content, input.encoding === "base64" ? "base64" : "utf8");
15
+ try {
16
+ if (input.createParents)
17
+ await mkdir(path.dirname(target), { recursive: true });
18
+ await writeFile(target, buffer);
19
+ return { bytesWritten: buffer.byteLength };
20
+ }
21
+ catch (err) {
22
+ throw wrapFsError("Fs.FileWrite: cannot write", target, err);
23
+ }
24
+ }
25
+ }
26
+ export function register() { }
27
+ export async function create(resource) {
28
+ return new FileWriteResource(resolveBase(resource.cwd));
29
+ }
@@ -0,0 +1,19 @@
1
+ /** A resource manifest as it reaches a controller's `create`: config fields sit
2
+ * at the top level beside `metadata`. Every fs kind shares the optional `cwd`. */
3
+ export interface FsManifest {
4
+ metadata: {
5
+ name: string;
6
+ module: string;
7
+ };
8
+ cwd?: string;
9
+ }
10
+ /** Resolve the base directory invoke paths are taken relative to. A relative
11
+ * `cwd` (and the default) resolves against the process working directory. */
12
+ export declare function resolveBase(cwd?: string): string;
13
+ /** Resolve an invoke `path` against the resource base. An absolute input path is
14
+ * used as-is. */
15
+ export declare function resolveTarget(base: string, target: string): string;
16
+ export declare function requirePath(kind: string, value: unknown): string;
17
+ /** Turn a Node fs error into an actionable, path-naming message that preserves
18
+ * the original code so callers (and tests) can branch on it. */
19
+ export declare function wrapFsError(action: string, target: string, err: unknown): Error;
@@ -0,0 +1,34 @@
1
+ import path from "node:path";
2
+ /** Resolve the base directory invoke paths are taken relative to. A relative
3
+ * `cwd` (and the default) resolves against the process working directory. */
4
+ export function resolveBase(cwd) {
5
+ return path.resolve(cwd ?? ".");
6
+ }
7
+ /** Resolve an invoke `path` against the resource base. An absolute input path is
8
+ * used as-is. */
9
+ export function resolveTarget(base, target) {
10
+ return path.resolve(base, target);
11
+ }
12
+ export function requirePath(kind, value) {
13
+ if (typeof value !== "string" || value.length === 0) {
14
+ throw new Error(`${kind}: 'path' input is required and must be a non-empty string`);
15
+ }
16
+ return value;
17
+ }
18
+ const REASONS = {
19
+ ENOENT: "no such file or directory",
20
+ EACCES: "permission denied",
21
+ EPERM: "operation not permitted",
22
+ EISDIR: "is a directory",
23
+ ENOTDIR: "not a directory",
24
+ EEXIST: "already exists",
25
+ ENOTEMPTY: "directory not empty",
26
+ };
27
+ /** Turn a Node fs error into an actionable, path-naming message that preserves
28
+ * the original code so callers (and tests) can branch on it. */
29
+ export function wrapFsError(action, target, err) {
30
+ const e = err;
31
+ const code = e?.code;
32
+ const reason = (code && REASONS[code]) ?? e?.message ?? String(err);
33
+ return new Error(`${action} '${target}': ${reason}${code ? ` (${code})` : ""}`, { cause: err });
34
+ }
@@ -0,0 +1,2 @@
1
+ export { requirePath, resolveBase, resolveTarget, wrapFsError } from "./fs-support.js";
2
+ export type { FsManifest } from "./fs-support.js";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // Public surface of @telorun/fs. Controllers load via the per-kind subpath
2
+ // exports (`#file`, `#file-edit`, …); this `.` entry exposes the shared path
3
+ // and error primitives so a future filesystem driver or another module can
4
+ // build on the same cwd-resolution and error contract — mirroring how
5
+ // @telorun/shell exports its host helpers/types for driver reuse.
6
+ export { requirePath, resolveBase, resolveTarget, wrapFsError } from "./fs-support.js";
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@telorun/fs",
3
+ "version": "0.1.0",
4
+ "description": "Telo fs module - local filesystem access via Node fs/promises.",
5
+ "keywords": [
6
+ "telo",
7
+ "fs",
8
+ "filesystem",
9
+ "file",
10
+ "directory"
11
+ ],
12
+ "author": "Bartosz Pasiński <bartosz.pasinski@codenet.pl>",
13
+ "license": "SEE LICENSE IN LICENSE",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/telorun/telo.git",
17
+ "directory": "modules/fs/nodejs"
18
+ },
19
+ "homepage": "https://github.com/telorun/telo#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/telorun/telo/issues"
22
+ },
23
+ "type": "module",
24
+ "main": "./dist/index.js",
25
+ "module": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "bun": "./src/index.ts",
31
+ "import": "./dist/index.js"
32
+ },
33
+ "./file": {
34
+ "bun": "./src/file-controller.ts",
35
+ "import": "./dist/file-controller.js"
36
+ },
37
+ "./file-write": {
38
+ "bun": "./src/file-write-controller.ts",
39
+ "import": "./dist/file-write-controller.js"
40
+ },
41
+ "./file-edit": {
42
+ "bun": "./src/file-edit-controller.ts",
43
+ "import": "./dist/file-edit-controller.js"
44
+ },
45
+ "./directory-listing": {
46
+ "bun": "./src/directory-listing-controller.ts",
47
+ "import": "./dist/directory-listing-controller.js"
48
+ },
49
+ "./directory-creation": {
50
+ "bun": "./src/directory-creation-controller.ts",
51
+ "import": "./dist/directory-creation-controller.js"
52
+ },
53
+ "./file-removal": {
54
+ "bun": "./src/file-removal-controller.ts",
55
+ "import": "./dist/file-removal-controller.js"
56
+ }
57
+ },
58
+ "files": [
59
+ "dist",
60
+ "src/**"
61
+ ],
62
+ "devDependencies": {
63
+ "@types/node": "^20.0.0",
64
+ "typescript": "^5.0.0",
65
+ "@telorun/sdk": "0.38.0"
66
+ },
67
+ "peerDependencies": {
68
+ "@telorun/sdk": "*"
69
+ },
70
+ "scripts": {
71
+ "build": "tsc -p tsconfig.lib.json"
72
+ }
73
+ }
@@ -0,0 +1,38 @@
1
+ import type { ResourceInstance } from "@telorun/sdk";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { FsManifest, requirePath, resolveBase, resolveTarget, wrapFsError } from "./fs-support.js";
4
+
5
+ interface DirectoryCreationInput {
6
+ path: string;
7
+ createParents?: boolean;
8
+ }
9
+
10
+ interface DirectoryCreationResult {
11
+ created: boolean;
12
+ }
13
+
14
+ class DirectoryCreationResource implements ResourceInstance<DirectoryCreationInput, DirectoryCreationResult> {
15
+ constructor(private readonly base: string) {}
16
+
17
+ async invoke(input: DirectoryCreationInput): Promise<DirectoryCreationResult> {
18
+ const target = resolveTarget(this.base, requirePath("Fs.DirectoryCreation", input?.path));
19
+ try {
20
+ if (input?.createParents) {
21
+ // Recursive mkdir returns the first created path, or undefined when the
22
+ // directory already existed — that's the created/no-op signal.
23
+ const first = await mkdir(target, { recursive: true });
24
+ return { created: first !== undefined };
25
+ }
26
+ await mkdir(target);
27
+ return { created: true };
28
+ } catch (err) {
29
+ throw wrapFsError("Fs.DirectoryCreation: cannot create", target, err);
30
+ }
31
+ }
32
+ }
33
+
34
+ export function register(): void {}
35
+
36
+ export async function create(resource: FsManifest): Promise<DirectoryCreationResource> {
37
+ return new DirectoryCreationResource(resolveBase(resource.cwd));
38
+ }
@@ -0,0 +1,68 @@
1
+ import type { ResourceInstance } from "@telorun/sdk";
2
+ import { lstat, readdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { FsManifest, resolveBase, resolveTarget, wrapFsError } from "./fs-support.js";
5
+
6
+ interface DirectoryListingInput {
7
+ path?: string;
8
+ recursive?: boolean;
9
+ }
10
+
11
+ interface Entry {
12
+ name: string;
13
+ path: string;
14
+ type: "file" | "directory" | "other";
15
+ size: number;
16
+ }
17
+
18
+ interface DirectoryListingResult {
19
+ entries: Entry[];
20
+ }
21
+
22
+ function classify(stats: { isFile(): boolean; isDirectory(): boolean }): Entry["type"] {
23
+ if (stats.isFile()) return "file";
24
+ if (stats.isDirectory()) return "directory";
25
+ return "other";
26
+ }
27
+
28
+ class DirectoryListingResource implements ResourceInstance<DirectoryListingInput, DirectoryListingResult> {
29
+ constructor(private readonly base: string) {}
30
+
31
+ async invoke(input: DirectoryListingInput): Promise<DirectoryListingResult> {
32
+ const root = input?.path ? resolveTarget(this.base, input.path) : this.base;
33
+ const entries: Entry[] = [];
34
+ await this.walk(root, Boolean(input?.recursive), entries);
35
+ return { entries };
36
+ }
37
+
38
+ private async walk(dir: string, recursive: boolean, out: Entry[]): Promise<void> {
39
+ let names: string[];
40
+ try {
41
+ names = await readdir(dir);
42
+ } catch (err) {
43
+ throw wrapFsError("Fs.DirectoryListing: cannot list", dir, err);
44
+ }
45
+ names.sort();
46
+ for (const name of names) {
47
+ const full = path.join(dir, name);
48
+ // lstat (not stat) so a broken or out-of-tree symlink is reported as
49
+ // "other" rather than throwing. Wrapped so a mid-walk race (entry removed
50
+ // after readdir) or permission error names the path like every other op.
51
+ let stats: Awaited<ReturnType<typeof lstat>>;
52
+ try {
53
+ stats = await lstat(full);
54
+ } catch (err) {
55
+ throw wrapFsError("Fs.DirectoryListing: cannot stat", full, err);
56
+ }
57
+ const type = classify(stats);
58
+ out.push({ name, path: path.relative(this.base, full), type, size: stats.size });
59
+ if (recursive && type === "directory") await this.walk(full, recursive, out);
60
+ }
61
+ }
62
+ }
63
+
64
+ export function register(): void {}
65
+
66
+ export async function create(resource: FsManifest): Promise<DirectoryListingResource> {
67
+ return new DirectoryListingResource(resolveBase(resource.cwd));
68
+ }
@@ -0,0 +1,34 @@
1
+ import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
2
+ import { readFile } from "node:fs/promises";
3
+ import { FsManifest, requirePath, resolveBase, resolveTarget, wrapFsError } from "./fs-support.js";
4
+
5
+ interface FileInput {
6
+ path: string;
7
+ encoding?: "utf8" | "base64";
8
+ }
9
+
10
+ interface FileResult {
11
+ content: string;
12
+ size: number;
13
+ }
14
+
15
+ class FileResource implements ResourceInstance<FileInput, FileResult> {
16
+ constructor(private readonly base: string) {}
17
+
18
+ async invoke(input: FileInput): Promise<FileResult> {
19
+ const target = resolveTarget(this.base, requirePath("Fs.File", input?.path));
20
+ try {
21
+ const buffer = await readFile(target);
22
+ const content = input.encoding === "base64" ? buffer.toString("base64") : buffer.toString("utf8");
23
+ return { content, size: buffer.byteLength };
24
+ } catch (err) {
25
+ throw wrapFsError("Fs.File: cannot read", target, err);
26
+ }
27
+ }
28
+ }
29
+
30
+ export function register(): void {}
31
+
32
+ export async function create(resource: FsManifest): Promise<FileResource> {
33
+ return new FileResource(resolveBase(resource.cwd));
34
+ }
@@ -0,0 +1,83 @@
1
+ import type { ResourceInstance } from "@telorun/sdk";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { FsManifest, requirePath, resolveBase, resolveTarget, wrapFsError } from "./fs-support.js";
4
+
5
+ interface FileEditInput {
6
+ path: string;
7
+ oldString: string;
8
+ newString: string;
9
+ replaceAll?: boolean;
10
+ }
11
+
12
+ interface FileEditResult {
13
+ replacements: number;
14
+ }
15
+
16
+ /** Byte offsets of every non-overlapping occurrence of `needle` in `haystack`. */
17
+ function findAll(haystack: Buffer, needle: Buffer): number[] {
18
+ const positions: number[] = [];
19
+ for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + needle.length)) {
20
+ positions.push(at);
21
+ }
22
+ return positions;
23
+ }
24
+
25
+ class FileEditResource implements ResourceInstance<FileEditInput, FileEditResult> {
26
+ constructor(private readonly base: string) {}
27
+
28
+ async invoke(input: FileEditInput): Promise<FileEditResult> {
29
+ const target = resolveTarget(this.base, requirePath("Fs.FileEdit", input?.path));
30
+ if (typeof input?.oldString !== "string" || input.oldString.length === 0) {
31
+ throw new Error("Fs.FileEdit: 'oldString' input is required and must be a non-empty string");
32
+ }
33
+ if (typeof input?.newString !== "string") {
34
+ throw new Error("Fs.FileEdit: 'newString' input is required and must be a string");
35
+ }
36
+
37
+ let buffer: Buffer;
38
+ try {
39
+ buffer = await readFile(target);
40
+ } catch (err) {
41
+ throw wrapFsError("Fs.FileEdit: cannot read", target, err);
42
+ }
43
+
44
+ // Operate on bytes, not decoded text: regions outside the match survive
45
+ // byte-for-byte (comments, !cel tags, and any non-UTF-8 content), and the
46
+ // literal search sidesteps regex / `$`-substitution.
47
+ const oldBytes = Buffer.from(input.oldString, "utf8");
48
+ const positions = findAll(buffer, oldBytes);
49
+ const count = positions.length;
50
+ if (count === 0) {
51
+ throw new Error(`Fs.FileEdit: oldString not found in '${target}'`);
52
+ }
53
+ if (count > 1 && !input.replaceAll) {
54
+ throw new Error(
55
+ `Fs.FileEdit: oldString matches ${count} times in '${target}'; set 'replaceAll: true' or use a more specific string`,
56
+ );
57
+ }
58
+
59
+ const newBytes = Buffer.from(input.newString, "utf8");
60
+ const replacements = input.replaceAll ? count : 1;
61
+ const parts: Buffer[] = [];
62
+ let cursor = 0;
63
+ for (let i = 0; i < replacements; i++) {
64
+ const at = positions[i];
65
+ parts.push(buffer.subarray(cursor, at), newBytes);
66
+ cursor = at + oldBytes.length;
67
+ }
68
+ parts.push(buffer.subarray(cursor));
69
+
70
+ try {
71
+ await writeFile(target, Buffer.concat(parts));
72
+ } catch (err) {
73
+ throw wrapFsError("Fs.FileEdit: cannot write", target, err);
74
+ }
75
+ return { replacements };
76
+ }
77
+ }
78
+
79
+ export function register(): void {}
80
+
81
+ export async function create(resource: FsManifest): Promise<FileEditResource> {
82
+ return new FileEditResource(resolveBase(resource.cwd));
83
+ }
@@ -0,0 +1,33 @@
1
+ import type { ResourceInstance } from "@telorun/sdk";
2
+ import { rm } from "node:fs/promises";
3
+ import { FsManifest, requirePath, resolveBase, resolveTarget, wrapFsError } from "./fs-support.js";
4
+
5
+ interface FileRemovalInput {
6
+ path: string;
7
+ recursive?: boolean;
8
+ }
9
+
10
+ interface FileRemovalResult {
11
+ removed: boolean;
12
+ }
13
+
14
+ class FileRemovalResource implements ResourceInstance<FileRemovalInput, FileRemovalResult> {
15
+ constructor(private readonly base: string) {}
16
+
17
+ async invoke(input: FileRemovalInput): Promise<FileRemovalResult> {
18
+ const target = resolveTarget(this.base, requirePath("Fs.FileRemoval", input?.path));
19
+ try {
20
+ // force:false so a missing path is surfaced (ENOENT), not swallowed.
21
+ await rm(target, { recursive: Boolean(input?.recursive), force: false });
22
+ return { removed: true };
23
+ } catch (err) {
24
+ throw wrapFsError("Fs.FileRemoval: cannot remove", target, err);
25
+ }
26
+ }
27
+ }
28
+
29
+ export function register(): void {}
30
+
31
+ export async function create(resource: FsManifest): Promise<FileRemovalResource> {
32
+ return new FileRemovalResource(resolveBase(resource.cwd));
33
+ }
@@ -0,0 +1,40 @@
1
+ import type { ResourceInstance } from "@telorun/sdk";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { FsManifest, requirePath, resolveBase, resolveTarget, wrapFsError } from "./fs-support.js";
5
+
6
+ interface FileWriteInput {
7
+ path: string;
8
+ content: string;
9
+ encoding?: "utf8" | "base64";
10
+ createParents?: boolean;
11
+ }
12
+
13
+ interface FileWriteResult {
14
+ bytesWritten: number;
15
+ }
16
+
17
+ class FileWriteResource implements ResourceInstance<FileWriteInput, FileWriteResult> {
18
+ constructor(private readonly base: string) {}
19
+
20
+ async invoke(input: FileWriteInput): Promise<FileWriteResult> {
21
+ const target = resolveTarget(this.base, requirePath("Fs.FileWrite", input?.path));
22
+ if (typeof input?.content !== "string") {
23
+ throw new Error("Fs.FileWrite: 'content' input is required and must be a string");
24
+ }
25
+ const buffer = Buffer.from(input.content, input.encoding === "base64" ? "base64" : "utf8");
26
+ try {
27
+ if (input.createParents) await mkdir(path.dirname(target), { recursive: true });
28
+ await writeFile(target, buffer);
29
+ return { bytesWritten: buffer.byteLength };
30
+ } catch (err) {
31
+ throw wrapFsError("Fs.FileWrite: cannot write", target, err);
32
+ }
33
+ }
34
+ }
35
+
36
+ export function register(): void {}
37
+
38
+ export async function create(resource: FsManifest): Promise<FileWriteResource> {
39
+ return new FileWriteResource(resolveBase(resource.cwd));
40
+ }
@@ -0,0 +1,46 @@
1
+ import path from "node:path";
2
+
3
+ /** A resource manifest as it reaches a controller's `create`: config fields sit
4
+ * at the top level beside `metadata`. Every fs kind shares the optional `cwd`. */
5
+ export interface FsManifest {
6
+ metadata: { name: string; module: string };
7
+ cwd?: string;
8
+ }
9
+
10
+ /** Resolve the base directory invoke paths are taken relative to. A relative
11
+ * `cwd` (and the default) resolves against the process working directory. */
12
+ export function resolveBase(cwd?: string): string {
13
+ return path.resolve(cwd ?? ".");
14
+ }
15
+
16
+ /** Resolve an invoke `path` against the resource base. An absolute input path is
17
+ * used as-is. */
18
+ export function resolveTarget(base: string, target: string): string {
19
+ return path.resolve(base, target);
20
+ }
21
+
22
+ export function requirePath(kind: string, value: unknown): string {
23
+ if (typeof value !== "string" || value.length === 0) {
24
+ throw new Error(`${kind}: 'path' input is required and must be a non-empty string`);
25
+ }
26
+ return value;
27
+ }
28
+
29
+ const REASONS: Record<string, string> = {
30
+ ENOENT: "no such file or directory",
31
+ EACCES: "permission denied",
32
+ EPERM: "operation not permitted",
33
+ EISDIR: "is a directory",
34
+ ENOTDIR: "not a directory",
35
+ EEXIST: "already exists",
36
+ ENOTEMPTY: "directory not empty",
37
+ };
38
+
39
+ /** Turn a Node fs error into an actionable, path-naming message that preserves
40
+ * the original code so callers (and tests) can branch on it. */
41
+ export function wrapFsError(action: string, target: string, err: unknown): Error {
42
+ const e = err as NodeJS.ErrnoException;
43
+ const code = e?.code;
44
+ const reason = (code && REASONS[code]) ?? e?.message ?? String(err);
45
+ return new Error(`${action} '${target}': ${reason}${code ? ` (${code})` : ""}`, { cause: err });
46
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ // Public surface of @telorun/fs. Controllers load via the per-kind subpath
2
+ // exports (`#file`, `#file-edit`, …); this `.` entry exposes the shared path
3
+ // and error primitives so a future filesystem driver or another module can
4
+ // build on the same cwd-resolution and error contract — mirroring how
5
+ // @telorun/shell exports its host helpers/types for driver reuse.
6
+ export { requirePath, resolveBase, resolveTarget, wrapFsError } from "./fs-support.js";
7
+ export type { FsManifest } from "./fs-support.js";