├── .editorconfig ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── biome.json ├── bun.lock ├── package.json ├── packages ├── cli │ ├── Cargo.toml │ ├── build.rs │ ├── src │ │ ├── archive.rs │ │ ├── blob.rs │ │ ├── build.rs │ │ ├── bundle.rs │ │ ├── cat.rs │ │ ├── check.rs │ │ ├── checkin.rs │ │ ├── checkout.rs │ │ ├── checksum.rs │ │ ├── children.rs │ │ ├── clean.rs │ │ ├── compress.rs │ │ ├── config.rs │ │ ├── decompress.rs │ │ ├── document.rs │ │ ├── download.rs │ │ ├── export.rs │ │ ├── extract.rs │ │ ├── format.rs │ │ ├── get.rs │ │ ├── health.rs │ │ ├── import.rs │ │ ├── index.rs │ │ ├── init.rs │ │ ├── lib.rs │ │ ├── lsp.rs │ │ ├── main.rs │ │ ├── metadata.rs │ │ ├── new.rs │ │ ├── object.rs │ │ ├── object │ │ │ ├── children.rs │ │ │ ├── get.rs │ │ │ ├── metadata.rs │ │ │ └── put.rs │ │ ├── outdated.rs │ │ ├── process.rs │ │ ├── process │ │ │ ├── cancel.rs │ │ │ ├── children.rs │ │ │ ├── get.rs │ │ │ ├── log.rs │ │ │ ├── metadata.rs │ │ │ ├── output.rs │ │ │ ├── put.rs │ │ │ ├── signal.rs │ │ │ ├── spawn.rs │ │ │ ├── status.rs │ │ │ └── wait.rs │ │ ├── progress.rs │ │ ├── pull.rs │ │ ├── push.rs │ │ ├── put.rs │ │ ├── remote.rs │ │ ├── remote │ │ │ ├── delete.rs │ │ │ ├── get.rs │ │ │ ├── list.rs │ │ │ └── put.rs │ │ ├── run.rs │ │ ├── run │ │ │ ├── signal.rs │ │ │ └── stdio.rs │ │ ├── server.rs │ │ ├── server │ │ │ ├── restart.rs │ │ │ ├── run.rs │ │ │ ├── start.rs │ │ │ ├── status.rs │ │ │ └── stop.rs │ │ ├── tag.rs │ │ ├── tag │ │ │ ├── delete.rs │ │ │ ├── get.rs │ │ │ ├── list.rs │ │ │ └── put.rs │ │ ├── tangram.ascii │ │ ├── tangram.rs │ │ ├── tangram │ │ │ └── update.rs │ │ ├── test.rs │ │ ├── tree.rs │ │ ├── update.rs │ │ ├── util.rs │ │ ├── view.rs │ │ ├── viewer.rs │ │ └── viewer │ │ │ ├── data.rs │ │ │ ├── help.rs │ │ │ ├── log.rs │ │ │ ├── log │ │ │ └── scroll.rs │ │ │ └── tree.rs │ └── tests │ │ ├── blob.rs │ │ ├── build.rs │ │ ├── bundle.rs │ │ ├── check.rs │ │ ├── checkin.rs │ │ ├── checkout.rs │ │ ├── checksum.rs │ │ ├── clean.rs │ │ ├── directory.rs │ │ ├── document.rs │ │ ├── format.rs │ │ ├── push.rs │ │ └── tag.rs ├── client │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── artifact.rs │ │ ├── artifact │ │ ├── data.rs │ │ ├── handle.rs │ │ ├── id.rs │ │ ├── kind.rs │ │ └── object.rs │ │ ├── blob.rs │ │ ├── blob │ │ ├── create.rs │ │ ├── data.rs │ │ ├── handle.rs │ │ ├── id.rs │ │ ├── object.rs │ │ └── read.rs │ │ ├── build.rs │ │ ├── builtin.rs │ │ ├── bytes.rs │ │ ├── check.rs │ │ ├── checkin.rs │ │ ├── checkout.rs │ │ ├── checksum.rs │ │ ├── clean.rs │ │ ├── command.rs │ │ ├── command │ │ ├── builder.rs │ │ ├── data.rs │ │ ├── handle.rs │ │ ├── id.rs │ │ └── object.rs │ │ ├── compiler.rs │ │ ├── diagnostic.rs │ │ ├── directory.rs │ │ ├── directory │ │ ├── builder.rs │ │ ├── data.rs │ │ ├── handle.rs │ │ ├── id.rs │ │ └── object.rs │ │ ├── document.rs │ │ ├── error.rs │ │ ├── export.rs │ │ ├── file.rs │ │ ├── file │ │ ├── builder.rs │ │ ├── data.rs │ │ ├── handle.rs │ │ ├── id.rs │ │ └── object.rs │ │ ├── format.rs │ │ ├── get.rs │ │ ├── graph.rs │ │ ├── graph │ │ ├── data.rs │ │ ├── handle.rs │ │ ├── id.rs │ │ └── object.rs │ │ ├── handle.rs │ │ ├── handle │ │ ├── either.rs │ │ └── ext.rs │ │ ├── health.rs │ │ ├── id.rs │ │ ├── import.rs │ │ ├── index.rs │ │ ├── lib.rs │ │ ├── location.rs │ │ ├── lockfile.rs │ │ ├── module.rs │ │ ├── module │ │ ├── data.rs │ │ └── import.rs │ │ ├── mutation.rs │ │ ├── object.rs │ │ ├── object │ │ ├── data.rs │ │ ├── get.rs │ │ ├── handle.rs │ │ ├── id.rs │ │ ├── kind.rs │ │ ├── metadata.rs │ │ ├── object.rs │ │ ├── put.rs │ │ ├── state.rs │ │ └── touch.rs │ │ ├── package.rs │ │ ├── pipe.rs │ │ ├── pipe │ │ ├── close.rs │ │ ├── create.rs │ │ ├── id.rs │ │ ├── read.rs │ │ └── write.rs │ │ ├── position.rs │ │ ├── process.rs │ │ ├── process │ │ ├── children.rs │ │ ├── children │ │ │ └── get.rs │ │ ├── data.rs │ │ ├── dequeue.rs │ │ ├── finish.rs │ │ ├── get.rs │ │ ├── heartbeat.rs │ │ ├── id.rs │ │ ├── log.rs │ │ ├── log │ │ │ ├── get.rs │ │ │ └── post.rs │ │ ├── metadata.rs │ │ ├── mount.rs │ │ ├── pty.rs │ │ ├── put.rs │ │ ├── signal.rs │ │ ├── signal │ │ │ ├── get.rs │ │ │ └── post.rs │ │ ├── spawn.rs │ │ ├── start.rs │ │ ├── state.rs │ │ ├── status.rs │ │ ├── stdio.rs │ │ ├── touch.rs │ │ └── wait.rs │ │ ├── progress.rs │ │ ├── pty.rs │ │ ├── pty │ │ ├── close.rs │ │ ├── create.rs │ │ ├── id.rs │ │ ├── read.rs │ │ ├── size.rs │ │ └── write.rs │ │ ├── pull.rs │ │ ├── push.rs │ │ ├── range.rs │ │ ├── reference.rs │ │ ├── referent.rs │ │ ├── remote.rs │ │ ├── remote │ │ ├── delete.rs │ │ ├── get.rs │ │ ├── list.rs │ │ └── put.rs │ │ ├── run.rs │ │ ├── symlink.rs │ │ ├── symlink │ │ ├── data.rs │ │ ├── handle.rs │ │ ├── id.rs │ │ └── object.rs │ │ ├── tag.rs │ │ ├── tag │ │ ├── delete.rs │ │ ├── get.rs │ │ ├── list.rs │ │ ├── pattern.rs │ │ └── put.rs │ │ ├── template.rs │ │ ├── user.rs │ │ ├── util.rs │ │ ├── util │ │ ├── arc.rs │ │ └── serde.rs │ │ ├── value.rs │ │ └── value │ │ ├── parse.rs │ │ └── print.rs ├── compiler │ ├── package.json │ ├── src │ │ ├── assert.ts │ │ ├── check.ts │ │ ├── completion.ts │ │ ├── definition.ts │ │ ├── diagnostics.ts │ │ ├── document.ts │ │ ├── error.ts │ │ ├── hover.ts │ │ ├── location.ts │ │ ├── log.ts │ │ ├── main.ts │ │ ├── module.ts │ │ ├── position.ts │ │ ├── range.ts │ │ ├── references.ts │ │ ├── rename.ts │ │ ├── symbols.ts │ │ ├── syscall.ts │ │ └── typescript.ts │ └── tsconfig.json ├── database │ ├── Cargo.toml │ └── src │ │ ├── either.rs │ │ ├── lib.rs │ │ ├── pool.rs │ │ ├── postgres.rs │ │ ├── row.rs │ │ ├── sqlite.rs │ │ ├── value.rs │ │ └── value │ │ ├── de.rs │ │ ├── json.rs │ │ └── ser.rs ├── either │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── futures │ ├── Cargo.toml │ └── src │ │ ├── attach.rs │ │ ├── either.rs │ │ ├── future.rs │ │ ├── lib.rs │ │ ├── read.rs │ │ ├── read │ │ └── shared_position_reader.rs │ │ ├── stream.rs │ │ ├── stream │ │ └── take_while_inclusive.rs │ │ ├── task.rs │ │ └── write.rs ├── http │ ├── Cargo.toml │ └── src │ │ ├── body.rs │ │ ├── body │ │ └── and_then_frame.rs │ │ ├── header.rs │ │ ├── header │ │ ├── accept_encoding.rs │ │ └── content_encoding.rs │ │ ├── idle.rs │ │ ├── layer.rs │ │ ├── layer │ │ ├── compression.rs │ │ └── tracing.rs │ │ ├── lib.rs │ │ ├── middleware.rs │ │ ├── request.rs │ │ ├── request │ │ └── builder.rs │ │ ├── response.rs │ │ ├── response │ │ └── builder.rs │ │ └── sse.rs ├── ignore │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── tests.rs ├── messenger │ ├── Cargo.toml │ └── src │ │ ├── acker.rs │ │ ├── either.rs │ │ ├── lib.rs │ │ ├── memory.rs │ │ └── nats.rs ├── runtime │ ├── package.json │ ├── src │ │ ├── args.ts │ │ ├── artifact.ts │ │ ├── assert.ts │ │ ├── blob.ts │ │ ├── build.ts │ │ ├── builtin.ts │ │ ├── checksum.ts │ │ ├── command.ts │ │ ├── directory.ts │ │ ├── encoding.ts │ │ ├── error.ts │ │ ├── file.ts │ │ ├── graph.ts │ │ ├── index.ts │ │ ├── log.ts │ │ ├── main.ts │ │ ├── module.ts │ │ ├── mutation.ts │ │ ├── object.ts │ │ ├── path.ts │ │ ├── process.ts │ │ ├── reference.ts │ │ ├── referent.ts │ │ ├── resolve.ts │ │ ├── run.ts │ │ ├── sleep.ts │ │ ├── start.ts │ │ ├── symlink.ts │ │ ├── syscall.ts │ │ ├── tag.ts │ │ ├── template.ts │ │ ├── util.ts │ │ └── value.ts │ ├── tangram.d.ts │ └── tsconfig.json ├── sandbox │ ├── Cargo.toml │ └── src │ │ ├── common.rs │ │ ├── darwin.rs │ │ ├── lib.rs │ │ ├── linux.rs │ │ ├── linux │ │ ├── guest.rs │ │ ├── init.rs │ │ └── root.rs │ │ ├── pty.rs │ │ └── stdio.rs ├── server │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── artifact.rs │ │ ├── blob.rs │ │ ├── blob │ │ ├── create.rs │ │ └── read.rs │ │ ├── cache.rs │ │ ├── cache │ │ └── tests.rs │ │ ├── check.rs │ │ ├── checkin.rs │ │ ├── checkin │ │ ├── input.rs │ │ ├── lockfile.rs │ │ ├── object.rs │ │ ├── output.rs │ │ └── unify.rs │ │ ├── checkout.rs │ │ ├── checksum.rs │ │ ├── clean.rs │ │ ├── compiler.rs │ │ ├── compiler │ │ ├── analysis.rs │ │ ├── check.rs │ │ ├── completion.rs │ │ ├── definition.rs │ │ ├── diagnostics.rs │ │ ├── document.rs │ │ ├── error.rs │ │ ├── format.rs │ │ ├── hover.rs │ │ ├── initialize.rs │ │ ├── jsonrpc.rs │ │ ├── load.rs │ │ ├── parse.rs │ │ ├── references.rs │ │ ├── rename.rs │ │ ├── resolve.rs │ │ ├── resolve │ │ │ └── tests.rs │ │ ├── symbols.rs │ │ ├── syscall.rs │ │ ├── syscall │ │ │ ├── document.rs │ │ │ ├── encoding.rs │ │ │ ├── log.rs │ │ │ └── module.rs │ │ ├── transpile.rs │ │ ├── version.rs │ │ └── workspace.rs │ │ ├── config.rs │ │ ├── database.rs │ │ ├── document.rs │ │ ├── export.rs │ │ ├── format.rs │ │ ├── health.rs │ │ ├── import.rs │ │ ├── index.rs │ │ ├── lib.rs │ │ ├── lockfile.rs │ │ ├── messenger.rs │ │ ├── module.rs │ │ ├── object.rs │ │ ├── object │ │ ├── complete.rs │ │ ├── get.rs │ │ ├── metadata.rs │ │ ├── put.rs │ │ └── touch.rs │ │ ├── pipe.rs │ │ ├── pipe │ │ ├── close.rs │ │ ├── create.rs │ │ ├── read.rs │ │ └── write.rs │ │ ├── process.rs │ │ ├── process │ │ ├── children.rs │ │ ├── children │ │ │ └── get.rs │ │ ├── dequeue.rs │ │ ├── finish.rs │ │ ├── get.rs │ │ ├── heartbeat.rs │ │ ├── log.rs │ │ ├── log │ │ │ ├── get.rs │ │ │ ├── post.rs │ │ │ └── reader.rs │ │ ├── metadata.rs │ │ ├── put.rs │ │ ├── signal.rs │ │ ├── signal │ │ │ ├── get.rs │ │ │ └── post.rs │ │ ├── spawn.rs │ │ ├── start.rs │ │ ├── status.rs │ │ ├── touch.rs │ │ └── wait.rs │ │ ├── progress.rs │ │ ├── pty.rs │ │ ├── pty │ │ ├── close.rs │ │ ├── create.rs │ │ ├── read.rs │ │ ├── size.rs │ │ └── write.rs │ │ ├── pull.rs │ │ ├── push.rs │ │ ├── reference.rs │ │ ├── reference │ │ └── get.rs │ │ ├── remote.rs │ │ ├── remote │ │ ├── delete.rs │ │ ├── get.rs │ │ ├── list.rs │ │ └── put.rs │ │ ├── runner.rs │ │ ├── runtime.rs │ │ ├── runtime │ │ ├── builtin.rs │ │ ├── builtin │ │ │ ├── archive.rs │ │ │ ├── bundle.rs │ │ │ ├── checksum.rs │ │ │ ├── compress.rs │ │ │ ├── decompress.rs │ │ │ ├── download.rs │ │ │ ├── extract.rs │ │ │ └── util.rs │ │ ├── darwin.rs │ │ ├── js.rs │ │ ├── js │ │ │ ├── error.rs │ │ │ ├── syscall.rs │ │ │ └── syscall │ │ │ │ ├── blob.rs │ │ │ │ ├── checksum.rs │ │ │ │ ├── encoding.rs │ │ │ │ ├── log.rs │ │ │ │ ├── magic.rs │ │ │ │ ├── object.rs │ │ │ │ ├── process.rs │ │ │ │ └── sleep.rs │ │ ├── linux.rs │ │ ├── proxy.rs │ │ └── util.rs │ │ ├── store.rs │ │ ├── store │ │ ├── fdb.rs │ │ ├── lmdb.rs │ │ ├── memory.rs │ │ └── s3.rs │ │ ├── tag.rs │ │ ├── tag │ │ ├── delete.rs │ │ ├── get.rs │ │ ├── list.rs │ │ ├── pull.rs │ │ └── put.rs │ │ ├── temp.rs │ │ ├── test.rs │ │ ├── user.rs │ │ ├── util.rs │ │ ├── util │ │ ├── fs.rs │ │ ├── iter.rs │ │ └── path.rs │ │ ├── vfs.rs │ │ ├── vfs │ │ └── provider.rs │ │ └── watchdog.rs ├── temp │ ├── Cargo.toml │ └── src │ │ ├── artifact.rs │ │ └── lib.rs ├── uri │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── reference.rs │ │ └── reference │ │ └── builder.rs ├── v8 │ ├── Cargo.toml │ └── src │ │ ├── convert.rs │ │ ├── convert │ │ ├── artifact.rs │ │ ├── blob.rs │ │ ├── builtin.rs │ │ ├── checksum.rs │ │ ├── command.rs │ │ ├── de.rs │ │ ├── directory.rs │ │ ├── error.rs │ │ ├── file.rs │ │ ├── graph.rs │ │ ├── module.rs │ │ ├── mutation.rs │ │ ├── object.rs │ │ ├── process.rs │ │ ├── reference.rs │ │ ├── referent.rs │ │ ├── ser.rs │ │ ├── serde.rs │ │ ├── symlink.rs │ │ ├── template.rs │ │ └── value.rs │ │ └── lib.rs ├── version │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── pattern.rs │ │ └── version.rs ├── vfs │ ├── Cargo.toml │ ├── examples │ │ └── hello.rs │ └── src │ │ ├── fuse.rs │ │ ├── fuse │ │ └── sys.rs │ │ ├── lib.rs │ │ ├── nfs.rs │ │ └── nfs │ │ ├── provider.rs │ │ ├── rpc.rs │ │ ├── types.rs │ │ └── xdr.rs └── vscode │ ├── .gitignore │ ├── .vscodeignore │ ├── LICENSE │ ├── extension.ts │ ├── package.json │ ├── tangram-javascript-jsdoc-injection.tmLanguage.json │ ├── tangram-javascript-language-configuration.json │ ├── tangram-javascript.tmLanguage.json │ ├── tangram-typescript-jsdoc-injection.tmLanguage.json │ ├── tangram-typescript-language-configuration.json │ ├── tangram-typescript.tmLanguage.json │ ├── tangram.png │ └── tsconfig.json ├── rustfmt.toml ├── scripts └── canary.ts ├── tangram.lock ├── tangram.svg └── tangram.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | [*.{json,js,md,rs,ts}] 9 | indent_size = 2 10 | indent_style = tab 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pending-snap 2 | .DS_Store 3 | /node_modules 4 | /packages/*/node_modules 5 | /release 6 | /target 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Tangram, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.6.3/schema.json", 3 | "json": { 4 | "formatter": { 5 | "indentStyle": "tab" 6 | } 7 | }, 8 | "linter": { 9 | "rules": { 10 | "complexity": { 11 | "noBannedTypes": "off" 12 | }, 13 | "correctness": { 14 | "noUnnecessaryContinue": "off" 15 | }, 16 | "style": { 17 | "noNonNullAssertion": "off", 18 | "noParameterAssign": "off", 19 | "noUselessElse": "off", 20 | "useConst": "off" 21 | }, 22 | "suspicious": { 23 | "noExplicitAny": "off", 24 | "noRedeclare": "off", 25 | "useIsArray": "off" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@biomejs/biome": "^1.9.4", 4 | "@types/bun": "^1.2.13", 5 | "esbuild": "^0.25.4", 6 | "typescript": "^5.8.3" 7 | }, 8 | "scripts": { 9 | "canary": "bun scripts/canary.ts", 10 | "check": "cargo clippy --all-features --all-targets --workspace && bun run --filter \"*\" check", 11 | "clean": "rm -rf node_modules target", 12 | "format": "cargo fmt --all && bun run --filter \"*\" format", 13 | "test": "cargo nextest run --workspace && bun run --filter \"*\" test", 14 | "tg": "cargo run -- -m client", 15 | "tgo": "cargo build --target aarch64-unknown-linux-gnu && orb sh -c './target/aarch64-unknown-linux-gnu/debug/tangram $@' -- -m client", 16 | "tgor": "cargo build --release --target aarch64-unknown-linux-gnu && orb sh -c './target/aarch64-unknown-linux-gnu/release/tangram $@' -- -m client", 17 | "tgorx": "cargo build --release --target aarch64-unknown-linux-gnu && orb sh -c './target/aarch64-unknown-linux-gnu/release/tangram $@' -- -m client run -b", 18 | "tgr": "cargo run --release -- -m client", 19 | "tgrs": "cargo run --release -- -m server", 20 | "tgrsx": "cargo run --release -- -m server run -b", 21 | "tgrx": "cargo run -- -m client run -b", 22 | "tgs": "cargo run -- -m server", 23 | "tgsx": "cargo run -- -m server run -b", 24 | "tgx": "cargo run -- -m client run -b" 25 | }, 26 | "workspaces": ["packages/compiler", "packages/runtime", "packages/vscode"] 27 | } 28 | -------------------------------------------------------------------------------- /packages/cli/src/archive.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | /// Archive an artifact. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(index = 1)] 9 | pub artifact: tg::artifact::Id, 10 | 11 | #[command(flatten)] 12 | pub build: crate::build::Options, 13 | 14 | #[arg(long)] 15 | pub format: tg::ArchiveFormat, 16 | 17 | #[arg(long)] 18 | pub compression: Option, 19 | } 20 | 21 | impl Cli { 22 | pub async fn command_archive(&mut self, args: Args) -> tg::Result<()> { 23 | let handle = self.handle().await?; 24 | let artifact = tg::Artifact::with_id(args.artifact); 25 | let format = args.format; 26 | let compression = args.compression; 27 | let command = tg::builtin::archive_command(&artifact, format, compression); 28 | let command = command.id(&handle).await?; 29 | let reference = tg::Reference::with_object(&command.into()); 30 | self.build(args.build, reference, vec![]).await?; 31 | Ok(()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/cli/src/blob.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// Create a blob. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(index = 1)] 9 | pub bytes: Option, 10 | } 11 | 12 | impl Cli { 13 | pub async fn command_blob(&mut self, args: Args) -> tg::Result<()> { 14 | let handle = self.handle().await?; 15 | let tg::blob::create::Output { blob, .. } = if let Some(bytes) = args.bytes { 16 | let reader = std::io::Cursor::new(bytes); 17 | handle.create_blob(reader).await? 18 | } else { 19 | let reader = tokio::io::stdin(); 20 | handle.create_blob(reader).await? 21 | }; 22 | println!("{blob}"); 23 | Ok(()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/cli/src/bundle.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | /// Bundle an artifact. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(index = 1)] 9 | pub artifact: tg::artifact::Id, 10 | 11 | #[command(flatten)] 12 | pub build: crate::build::Options, 13 | } 14 | 15 | impl Cli { 16 | pub async fn command_bundle(&mut self, args: Args) -> tg::Result<()> { 17 | let handle = self.handle().await?; 18 | let artifact = tg::Artifact::with_id(args.artifact); 19 | let command = tg::builtin::bundle_command(&artifact); 20 | let command = command.id(&handle).await?; 21 | let reference = tg::Reference::with_object(&command.into()); 22 | self.build(args.build, reference, vec![]).await?; 23 | Ok(()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/cli/src/children.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | use tangram_either::Either; 4 | 5 | /// Get the children. 6 | #[derive(Clone, Debug, clap::Args)] 7 | #[group(skip)] 8 | pub struct Args { 9 | /// The object or process. 10 | #[arg(index = 1, default_value = ".")] 11 | pub reference: tg::Reference, 12 | } 13 | 14 | impl Cli { 15 | pub async fn command_children(&mut self, args: Args) -> tg::Result<()> { 16 | let handle = self.handle().await?; 17 | let referent = self.get_reference(&args.reference).await?; 18 | match referent.item { 19 | Either::Left(process) => { 20 | let args = crate::process::children::Args { 21 | length: None, 22 | position: None, 23 | process: process.id().clone(), 24 | remote: None, 25 | size: None, 26 | }; 27 | self.command_process_children(args).await?; 28 | }, 29 | Either::Right(object) => { 30 | let args = crate::object::children::Args { 31 | object: object.id(&handle).await?.clone(), 32 | }; 33 | self.command_object_children(args).await?; 34 | }, 35 | } 36 | Ok(()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/cli/src/clean.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// Remove unused processes and objects. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args {} 8 | 9 | impl Cli { 10 | pub async fn command_clean(&mut self, _args: Args) -> tg::Result<()> { 11 | let handle = self.handle().await?; 12 | let stream = handle.clean().await?; 13 | self.render_progress_stream(stream).await?; 14 | Ok(()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/cli/src/compress.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | /// Compress a blob or a file. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(index = 1)] 9 | pub blob: tg::blob::Id, 10 | 11 | #[command(flatten)] 12 | pub build: crate::build::Options, 13 | 14 | #[arg(short, long)] 15 | pub format: tg::CompressionFormat, 16 | } 17 | 18 | impl Cli { 19 | pub async fn command_compress(&mut self, args: Args) -> tg::Result<()> { 20 | let handle = self.handle().await?; 21 | let blob = tg::Blob::with_id(args.blob); 22 | let format = args.format; 23 | let command = tg::builtin::compress_command(&blob, format); 24 | let command = command.id(&handle).await?; 25 | let reference = tg::Reference::with_object(&command.into()); 26 | self.build(args.build, reference, vec![]).await?; 27 | Ok(()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/cli/src/decompress.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | /// Decompress a blob or a file. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(index = 1)] 9 | pub blob: tg::blob::Id, 10 | 11 | #[command(flatten)] 12 | pub build: crate::build::Options, 13 | } 14 | 15 | impl Cli { 16 | pub async fn command_decompress(&mut self, args: Args) -> tg::Result<()> { 17 | let handle = self.handle().await?; 18 | let blob = tg::Blob::with_id(args.blob); 19 | let command = tg::builtin::decompress_command(&blob); 20 | let command = command.id(&handle).await?; 21 | let reference = tg::Reference::with_object(&command.into()); 22 | self.build(args.build, reference, vec![]).await?; 23 | Ok(()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/cli/src/download.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | use url::Url; 4 | 5 | /// Download a blob or an artifact. 6 | #[derive(Clone, Debug, clap::Args)] 7 | #[group(skip)] 8 | pub struct Args { 9 | #[command(flatten)] 10 | pub build: crate::build::Options, 11 | 12 | #[arg(long)] 13 | pub mode: Option, 14 | 15 | #[arg(index = 1)] 16 | pub url: Url, 17 | } 18 | 19 | impl Cli { 20 | pub async fn command_download(&mut self, args: Args) -> tg::Result<()> { 21 | let handle = self.handle().await?; 22 | let options = tg::DownloadOptions { mode: args.mode }; 23 | let command = tg::builtin::download_command(&args.url, Some(options)); 24 | let command = command.id(&handle).await?; 25 | let reference = tg::Reference::with_object(&command.into()); 26 | self.build(args.build, reference, vec![]).await?; 27 | Ok(()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/cli/src/extract.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | /// Extract an artifact from a blob. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(index = 1)] 9 | pub blob: tg::blob::Id, 10 | 11 | #[command(flatten)] 12 | pub build: crate::build::Options, 13 | } 14 | 15 | impl Cli { 16 | pub async fn command_extract(&mut self, args: Args) -> tg::Result<()> { 17 | let handle = self.handle().await?; 18 | let blob = tg::Blob::with_id(args.blob); 19 | let command = tg::builtin::extract_command(&blob); 20 | let command = command.id(&handle).await?; 21 | let reference = tg::Reference::with_object(&command.into()); 22 | self.build(args.build, reference, vec![]).await?; 23 | Ok(()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/cli/src/format.rs: -------------------------------------------------------------------------------- 1 | use crate::{Cli, util::infer_module_kind}; 2 | use std::path::PathBuf; 3 | use tangram_client::{self as tg, prelude::*}; 4 | 5 | /// Format a package. 6 | #[derive(Clone, Debug, clap::Args)] 7 | #[group(skip)] 8 | pub struct Args { 9 | #[arg(index = 1, default_value = ".")] 10 | pub path: PathBuf, 11 | } 12 | 13 | impl Cli { 14 | pub async fn command_format(&mut self, args: Args) -> tg::Result<()> { 15 | let handle = self.handle().await?; 16 | 17 | // Get the absolute path. 18 | let path = std::path::absolute(&args.path) 19 | .map_err(|source| tg::error!(!source, "failed to get the absolute path"))?; 20 | 21 | // Get the module kind. 22 | let kind = infer_module_kind(&path).unwrap_or(tg::module::Kind::Artifact); 23 | 24 | // Create a module. 25 | let module = tg::module::Data { 26 | referent: tg::Referent::with_item(tg::module::data::Item::Path(path)), 27 | kind, 28 | }; 29 | 30 | // Format the package. 31 | let arg = tg::format::Arg { module }; 32 | handle.format(arg).await?; 33 | 34 | Ok(()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/cli/src/health.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// Get the server's health. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args {} 8 | 9 | impl Cli { 10 | pub async fn command_health(&mut self, _args: Args) -> tg::Result<()> { 11 | let handle = self.handle().await?; 12 | let health = handle.health().await?; 13 | let health = serde_json::to_string_pretty(&health) 14 | .map_err(|source| tg::error!(!source, "failed to serialize the health"))?; 15 | println!("{health}"); 16 | Ok(()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/cli/src/import.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use futures::{StreamExt as _, TryStreamExt as _, stream}; 3 | use std::pin::pin; 4 | use tangram_client::{self as tg, prelude::*}; 5 | 6 | /// Import processes and objects. 7 | #[derive(Clone, Debug, clap::Args)] 8 | #[group(skip)] 9 | pub struct Args { 10 | #[allow(clippy::option_option)] 11 | #[arg(short, long)] 12 | pub remote: Option>, 13 | } 14 | 15 | impl Cli { 16 | pub async fn command_import(&mut self, args: Args) -> tg::Result<()> { 17 | let handle = self.handle().await?; 18 | 19 | // Get the remote. 20 | let remote = args 21 | .remote 22 | .map(|option| option.unwrap_or_else(|| "default".to_owned())); 23 | 24 | // Create the export stream. 25 | let stdin = tokio::io::stdin(); 26 | let stream = stream::try_unfold(stdin, |mut reader| async move { 27 | let Some(item) = tg::export::Item::from_reader(&mut reader).await? else { 28 | return Ok(None); 29 | }; 30 | Ok(Some((item, reader))) 31 | }) 32 | .boxed(); 33 | 34 | // Import. 35 | let arg = tg::import::Arg { 36 | items: vec![], 37 | remote, 38 | }; 39 | let stream = handle.import(arg, stream).await?; 40 | 41 | let mut stream = pin!(stream); 42 | while let Some(event) = stream.try_next().await? { 43 | let event = tangram_http::sse::Event::try_from(event)?; 44 | println!("{event}"); 45 | } 46 | 47 | Ok(()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/cli/src/index.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// Index processes and objects. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args {} 8 | 9 | impl Cli { 10 | pub async fn command_index(&mut self, _args: Args) -> tg::Result<()> { 11 | let handle = self.handle().await?; 12 | let stream = handle.index().await?; 13 | self.render_progress_stream(stream).await?; 14 | Ok(()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/cli/src/lsp.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// Run the language server. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args {} 8 | 9 | impl Cli { 10 | pub async fn command_lsp(&mut self, _args: Args) -> tg::Result<()> { 11 | let handle = self.handle().await?; 12 | 13 | let stdin = Box::new(tokio::io::BufReader::new(tokio::io::stdin())); 14 | let stdout = Box::new(tokio::io::stdout()); 15 | 16 | handle.lsp(stdin, stdout).await?; 17 | 18 | Ok(()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use tangram_cli::Cli; 2 | 3 | fn main() -> std::process::ExitCode { 4 | Cli::main() 5 | } 6 | -------------------------------------------------------------------------------- /packages/cli/src/new.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use std::path::PathBuf; 3 | use tangram_client as tg; 4 | 5 | /// Create a new package. 6 | #[derive(Clone, Debug, clap::Args)] 7 | #[group(skip)] 8 | pub struct Args { 9 | #[arg(index = 1)] 10 | pub path: Option, 11 | } 12 | 13 | impl Cli { 14 | pub async fn command_new(&mut self, args: Args) -> tg::Result<()> { 15 | let args = crate::init::Args { path: args.path }; 16 | self.command_init(args).await?; 17 | Ok(()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/cli/src/object.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | pub mod children; 5 | pub mod get; 6 | pub mod metadata; 7 | pub mod put; 8 | 9 | /// Manage objects. 10 | #[derive(Clone, Debug, clap::Args)] 11 | #[group(skip)] 12 | pub struct Args { 13 | #[command(subcommand)] 14 | pub command: Command, 15 | } 16 | 17 | #[derive(Clone, Debug, clap::Subcommand)] 18 | pub enum Command { 19 | Children(self::children::Args), 20 | Get(self::get::Args), 21 | Metadata(self::metadata::Args), 22 | Put(self::put::Args), 23 | } 24 | 25 | impl Cli { 26 | pub async fn command_object(&mut self, args: Args) -> tg::Result<()> { 27 | match args.command { 28 | Command::Children(args) => { 29 | self.command_object_children(args).await?; 30 | }, 31 | Command::Get(args) => { 32 | self.command_object_get(args).await?; 33 | }, 34 | Command::Metadata(args) => { 35 | self.command_object_metadata(args).await?; 36 | }, 37 | Command::Put(args) => { 38 | self.command_object_put(args).await?; 39 | }, 40 | } 41 | Ok(()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/cli/src/object/children.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | /// Get an object's children. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(index = 1)] 9 | pub object: tg::object::Id, 10 | } 11 | 12 | impl Cli { 13 | pub async fn command_object_children(&mut self, args: Args) -> tg::Result<()> { 14 | let handle = self.handle().await?; 15 | 16 | // Print the children. 17 | let object = tg::Object::with_id(args.object); 18 | let data = object.data(&handle).await?; 19 | for child in data.children() { 20 | println!("{child}"); 21 | } 22 | 23 | Ok(()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/cli/src/object/metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// Get object metadata. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(index = 1)] 9 | pub object: tg::object::Id, 10 | 11 | #[arg(long)] 12 | pub pretty: Option, 13 | } 14 | 15 | impl Cli { 16 | pub async fn command_object_metadata(&mut self, args: Args) -> tg::Result<()> { 17 | let handle = self.handle().await?; 18 | let metadata = handle.get_object_metadata(&args.object).await.map_err( 19 | |source| tg::error!(!source, %id = args.object, "failed to get the object metadata"), 20 | )?; 21 | Self::output_json(&metadata, args.pretty).await?; 22 | Ok(()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/cli/src/object/put.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | use tokio::io::AsyncReadExt as _; 4 | 5 | /// Put an object. 6 | #[derive(Clone, Debug, clap::Args)] 7 | #[group(skip)] 8 | pub struct Args { 9 | #[arg(index = 1)] 10 | pub bytes: Option, 11 | 12 | #[arg(index = 1)] 13 | pub id: Option, 14 | 15 | #[arg(short, long)] 16 | pub kind: Option, 17 | } 18 | 19 | impl Cli { 20 | pub async fn command_object_put(&mut self, args: Args) -> tg::Result<()> { 21 | let handle = self.handle().await?; 22 | let bytes = if let Some(bytes) = args.bytes { 23 | bytes.into_bytes().into() 24 | } else { 25 | let mut bytes = Vec::new(); 26 | tokio::io::stdin() 27 | .read_to_end(&mut bytes) 28 | .await 29 | .map_err(|source| tg::error!(!source, "failed to read stdin"))?; 30 | bytes.into() 31 | }; 32 | let id = if let Some(id) = args.id { 33 | id 34 | } else { 35 | let kind = args.kind.ok_or_else(|| tg::error!("kind must be set"))?; 36 | tg::object::Id::new(kind, &bytes) 37 | }; 38 | let arg = tg::object::put::Arg { bytes }; 39 | handle.put_object(&id, arg).await?; 40 | println!("{id}"); 41 | Ok(()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/cli/src/outdated.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | /// Get a package's outdated dependencies. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | /// If this flag is set, the package's lockfile will not be updated. 9 | #[arg(long)] 10 | pub locked: bool, 11 | 12 | #[arg(index = 1, default_value = ".")] 13 | pub package: tg::Reference, 14 | } 15 | 16 | impl Cli { 17 | pub async fn command_outdated(&mut self, _args: Args) -> tg::Result<()> { 18 | Err(tg::error!("unimplemented")) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/cli/src/process/cancel.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// Cancel a process. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(short, long)] 9 | pub force: bool, 10 | 11 | #[arg(index = 1)] 12 | pub process: tg::process::Id, 13 | 14 | #[allow(clippy::option_option)] 15 | #[arg(short, long)] 16 | pub remote: Option>, 17 | } 18 | 19 | impl Cli { 20 | pub async fn command_process_cancel(&mut self, args: Args) -> tg::Result<()> { 21 | let handle = self.handle().await?; 22 | 23 | // Get the remote. 24 | let remote = args 25 | .remote 26 | .map(|option| option.unwrap_or_else(|| "default".to_owned())); 27 | 28 | // Cancel the process. 29 | let arg = tg::process::finish::Arg { 30 | checksum: None, 31 | error: Some(tg::error!( 32 | code = tg::error::Code::Cancelation, 33 | "the process was explicitly canceled" 34 | )), 35 | exit: 1, 36 | force: args.force, 37 | output: None, 38 | remote, 39 | }; 40 | handle.finish_process(&args.process, arg).await?; 41 | 42 | Ok(()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/cli/src/process/children.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use futures::TryStreamExt as _; 3 | use std::pin::pin; 4 | use tangram_client::{self as tg, prelude::*}; 5 | 6 | /// Get a process's children. 7 | #[derive(Clone, Debug, clap::Args)] 8 | #[group(skip)] 9 | pub struct Args { 10 | #[arg(index = 1)] 11 | pub process: tg::process::Id, 12 | 13 | #[arg(long)] 14 | pub length: Option, 15 | 16 | #[arg(long)] 17 | pub position: Option, 18 | 19 | #[allow(clippy::option_option)] 20 | #[arg(short, long)] 21 | pub remote: Option>, 22 | 23 | #[arg(long)] 24 | pub size: Option, 25 | } 26 | 27 | impl Cli { 28 | pub async fn command_process_children(&mut self, args: Args) -> tg::Result<()> { 29 | let handle = self.handle().await?; 30 | 31 | // Get the children. 32 | let remote = args 33 | .remote 34 | .map(|option| option.unwrap_or_else(|| "default".to_owned())); 35 | let arg = tg::process::children::get::Arg { 36 | position: args.position.map(std::io::SeekFrom::Start), 37 | length: args.length, 38 | remote, 39 | size: args.size, 40 | }; 41 | let stream = handle.get_process_children(&args.process, arg).await?; 42 | 43 | // Print the children. 44 | let mut stream = pin!(stream); 45 | while let Some(chunk) = stream.try_next().await? { 46 | for child in chunk.data { 47 | println!("{child}"); 48 | } 49 | } 50 | 51 | Ok(()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/cli/src/process/get.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// Get a process. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(long)] 9 | pub pretty: Option, 10 | 11 | #[arg(index = 1)] 12 | pub process: tg::process::Id, 13 | } 14 | 15 | impl Cli { 16 | pub async fn command_process_get(&mut self, args: Args) -> tg::Result<()> { 17 | let handle = self.handle().await?; 18 | let tg::process::get::Output { data, .. } = handle.get_process(&args.process).await?; 19 | Self::output_json(&data, args.pretty).await?; 20 | Ok(()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/cli/src/process/metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// Get process metadata. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(index = 1)] 9 | pub process: tg::process::Id, 10 | 11 | #[arg(long)] 12 | pub pretty: Option, 13 | } 14 | 15 | impl Cli { 16 | pub async fn command_process_metadata(&mut self, args: Args) -> tg::Result<()> { 17 | let handle = self.handle().await?; 18 | let metadata = handle.get_process_metadata(&args.process).await.map_err( 19 | |source| tg::error!(!source, %id = args.process, "failed to get the process metadata"), 20 | )?; 21 | Self::output_json(&metadata, args.pretty).await?; 22 | Ok(()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/cli/src/process/output.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use std::io::IsTerminal as _; 3 | use tangram_client as tg; 4 | 5 | /// Get a process's output. 6 | #[derive(Clone, Debug, clap::Args)] 7 | #[group(skip)] 8 | pub struct Args { 9 | #[arg(index = 1)] 10 | pub process: tg::process::Id, 11 | } 12 | 13 | impl Cli { 14 | pub async fn command_process_output(&mut self, args: Args) -> tg::Result<()> { 15 | let handle = self.handle().await?; 16 | 17 | // Get the process. 18 | let process = tg::Process::new(args.process, None, None, None, None); 19 | 20 | // Get the output. 21 | let output = process.output(&handle).await?; 22 | 23 | // Print the output. 24 | let stdout = std::io::stdout(); 25 | let output = if stdout.is_terminal() { 26 | let options = tg::value::print::Options { 27 | recursive: false, 28 | style: tg::value::print::Style::Pretty { indentation: " " }, 29 | }; 30 | output.print(options) 31 | } else { 32 | output.to_string() 33 | }; 34 | println!("{output}"); 35 | 36 | Ok(()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/cli/src/process/put.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | use tokio::io::AsyncReadExt as _; 4 | 5 | // Put a process. 6 | #[derive(Clone, Debug, clap::Args)] 7 | #[group(skip)] 8 | pub struct Args { 9 | #[arg(index = 2)] 10 | pub bytes: Option, 11 | 12 | #[arg(index = 1)] 13 | pub id: tg::process::Id, 14 | } 15 | 16 | impl Cli { 17 | pub async fn command_process_put(&mut self, args: Args) -> tg::Result<()> { 18 | let handle = self.handle().await?; 19 | let bytes = if let Some(bytes) = args.bytes { 20 | bytes 21 | } else { 22 | let mut bytes = String::new(); 23 | tokio::io::stdin() 24 | .read_to_string(&mut bytes) 25 | .await 26 | .map_err(|source| tg::error!(!source, "failed to read stdin"))?; 27 | bytes 28 | }; 29 | let data = serde_json::from_str(&bytes) 30 | .map_err(|source| tg::error!(!source, "failed to deseralize the data"))?; 31 | let arg = tg::process::put::Arg { data }; 32 | handle.put_process(&args.id, arg).await?; 33 | Ok(()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/cli/src/process/signal.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// Signal a process. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(index = 1)] 9 | pub process: tg::process::Id, 10 | 11 | #[arg(long, short, default_value = "INT")] 12 | pub signal: tg::process::Signal, 13 | 14 | #[allow(clippy::option_option)] 15 | #[arg(short, long)] 16 | pub remote: Option>, 17 | } 18 | 19 | impl Cli { 20 | pub async fn command_process_signal(&mut self, args: Args) -> tg::Result<()> { 21 | let handle = self.handle().await?; 22 | 23 | // Get the remote. 24 | let remote = args 25 | .remote 26 | .map(|option| option.unwrap_or_else(|| "default".to_owned())); 27 | 28 | // Signal the process. 29 | let arg = tg::process::signal::post::Arg { 30 | remote, 31 | signal: args.signal, 32 | }; 33 | handle.signal_process(&args.process, arg).await?; 34 | 35 | Ok(()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/cli/src/process/status.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use futures::StreamExt as _; 3 | use std::pin::pin; 4 | use tangram_client::{self as tg, prelude::*}; 5 | 6 | /// Get a process's status. 7 | #[derive(Clone, Debug, clap::Args)] 8 | #[group(skip)] 9 | pub struct Args { 10 | #[arg(index = 1)] 11 | pub process: tg::process::Id, 12 | } 13 | 14 | impl Cli { 15 | pub async fn command_process_status(&mut self, args: Args) -> tg::Result<()> { 16 | let handle = self.handle().await?; 17 | 18 | // Get the stream. 19 | let stream = handle.get_process_status(&args.process).await?; 20 | 21 | // Print the status. 22 | let mut stream = pin!(stream); 23 | while let Some(status) = stream.next().await { 24 | let status = status.map_err(|source| tg::error!(!source, "expected a status"))?; 25 | println!("{status}"); 26 | } 27 | 28 | Ok(()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/cli/src/process/wait.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// Wait for a process to finish. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(long)] 9 | pub pretty: Option, 10 | 11 | #[arg(index = 1)] 12 | pub process: tg::process::Id, 13 | } 14 | 15 | impl Cli { 16 | pub async fn command_process_wait(&mut self, args: Args) -> tg::Result<()> { 17 | let handle = self.handle().await?; 18 | let output = handle.wait_process(&args.process).await?; 19 | Self::output_json(&output, args.pretty).await?; 20 | Ok(()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/cli/src/put.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | use tangram_either::Either; 4 | 5 | /// Put a process or an object. 6 | #[derive(Clone, Debug, clap::Args)] 7 | #[group(skip)] 8 | pub struct Args { 9 | #[arg(index = 2)] 10 | pub bytes: Option, 11 | 12 | #[arg(index = 1)] 13 | pub id: Option>, 14 | 15 | #[arg(short, long)] 16 | pub kind: Option, 17 | } 18 | 19 | impl Cli { 20 | pub async fn command_put(&mut self, args: Args) -> tg::Result<()> { 21 | match (args.id, args.kind) { 22 | (Some(Either::Left(id)), None) => { 23 | let args = crate::process::put::Args { 24 | bytes: args.bytes, 25 | id, 26 | }; 27 | self.command_process_put(args).await?; 28 | }, 29 | (id, kind) if id.is_none() || id.as_ref().is_some_and(Either::is_right) => { 30 | let args = crate::object::put::Args { 31 | bytes: args.bytes, 32 | id: id.map(Either::unwrap_right), 33 | kind, 34 | }; 35 | self.command_object_put(args).await?; 36 | }, 37 | _ => { 38 | return Err(tg::error!("invalid args")); 39 | }, 40 | } 41 | Ok(()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/cli/src/remote.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | pub mod delete; 5 | pub mod get; 6 | pub mod list; 7 | pub mod put; 8 | 9 | /// Manage remotes. 10 | #[derive(Clone, Debug, clap::Args)] 11 | #[group(skip)] 12 | pub struct Args { 13 | #[command(subcommand)] 14 | pub command: Command, 15 | } 16 | 17 | #[derive(Clone, Debug, clap::Subcommand)] 18 | pub enum Command { 19 | Delete(self::delete::Args), 20 | Get(self::get::Args), 21 | List(self::list::Args), 22 | Put(self::put::Args), 23 | } 24 | 25 | impl Cli { 26 | pub async fn command_remote(&mut self, args: Args) -> tg::Result<()> { 27 | match args.command { 28 | Command::Delete(args) => { 29 | self.command_remote_delete(args).await?; 30 | }, 31 | Command::Get(args) => { 32 | self.command_remote_get(args).await?; 33 | }, 34 | Command::List(args) => { 35 | self.command_remote_list(args).await?; 36 | }, 37 | Command::Put(args) => { 38 | self.command_remote_put(args).await?; 39 | }, 40 | } 41 | Ok(()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/cli/src/remote/delete.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// Delete a remote. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(index = 1)] 9 | pub name: String, 10 | } 11 | 12 | impl Cli { 13 | pub async fn command_remote_delete(&mut self, args: Args) -> tg::Result<()> { 14 | let handle = self.handle().await?; 15 | handle.delete_remote(&args.name).await?; 16 | Ok(()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/cli/src/remote/get.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// Get a remote. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(index = 1)] 9 | pub name: String, 10 | } 11 | 12 | impl Cli { 13 | pub async fn command_remote_get(&mut self, args: Args) -> tg::Result<()> { 14 | let handle = self.handle().await?; 15 | let remote = handle 16 | .try_get_remote(&args.name) 17 | .await? 18 | .ok_or_else(|| tg::error!("failed to find the remote"))?; 19 | println!("{}", remote.url); 20 | Ok(()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/cli/src/remote/list.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// List remotes. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args {} 8 | 9 | impl Cli { 10 | pub async fn command_remote_list(&mut self, _args: Args) -> tg::Result<()> { 11 | let handle = self.handle().await?; 12 | let arg = tg::remote::list::Arg::default(); 13 | let remotes = handle.list_remotes(arg).await?; 14 | for remote in remotes.data { 15 | println!("{} {}", remote.name, remote.url); 16 | } 17 | Ok(()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/cli/src/remote/put.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | use url::Url; 4 | 5 | /// Put a remote. 6 | #[derive(Clone, Debug, clap::Args)] 7 | #[group(skip)] 8 | pub struct Args { 9 | #[arg(index = 1)] 10 | pub name: String, 11 | 12 | #[arg(index = 2)] 13 | pub url: Url, 14 | } 15 | 16 | impl Cli { 17 | pub async fn command_remote_put(&mut self, args: Args) -> tg::Result<()> { 18 | let handle = self.handle().await?; 19 | let name = args.name; 20 | let url = args.url; 21 | let arg = tg::remote::put::Arg { url }; 22 | handle.put_remote(&name, arg).await?; 23 | Ok(()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/cli/src/server.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | pub mod restart; 5 | pub mod run; 6 | pub mod start; 7 | pub mod status; 8 | pub mod stop; 9 | 10 | /// Manage the server. 11 | #[derive(Clone, Debug, clap::Args)] 12 | #[group(skip)] 13 | pub struct Args { 14 | #[command(subcommand)] 15 | pub command: Command, 16 | } 17 | 18 | #[derive(Clone, Debug, clap::Subcommand)] 19 | pub enum Command { 20 | Restart(self::restart::Args), 21 | Run(self::run::Args), 22 | Start(self::start::Args), 23 | Status(self::status::Args), 24 | Stop(self::stop::Args), 25 | } 26 | 27 | impl Cli { 28 | pub async fn command_server(&mut self, args: Args) -> tg::Result<()> { 29 | match args.command { 30 | Command::Restart(args) => { 31 | self.command_server_restart(args).await?; 32 | }, 33 | Command::Run(args) => { 34 | self.command_server_run(args).await?; 35 | }, 36 | Command::Start(args) => { 37 | self.command_server_start(args).await?; 38 | }, 39 | Command::Status(args) => { 40 | self.command_server_status(args).await?; 41 | }, 42 | Command::Stop(args) => { 43 | self.command_server_stop(args).await?; 44 | }, 45 | } 46 | Ok(()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/cli/src/server/restart.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | /// Stop the server. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args {} 8 | 9 | impl Cli { 10 | pub async fn command_server_restart(&mut self, _args: Args) -> tg::Result<()> { 11 | self.stop_server().await?; 12 | self.start_server().await?; 13 | Ok(()) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/cli/src/server/run.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | /// Run the server in the foreground. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args {} 8 | 9 | impl Cli { 10 | pub async fn command_server_run(&mut self, _args: Args) -> tg::Result<()> { 11 | let handle = self.handle().await?; 12 | 13 | // Get the server. 14 | let server = handle.as_ref().unwrap_right(); 15 | 16 | // Spawn a task to stop the server on the first interrupt signal and exit the process on the second. 17 | tokio::spawn({ 18 | let server = server.clone(); 19 | async move { 20 | tokio::signal::ctrl_c().await.unwrap(); 21 | server.stop(); 22 | drop(server); 23 | tokio::signal::ctrl_c().await.unwrap(); 24 | std::process::exit(130); 25 | } 26 | }); 27 | 28 | // Wait for the server. 29 | server.wait().await; 30 | 31 | Ok(()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/cli/src/server/start.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | /// Start the server. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args {} 8 | 9 | impl Cli { 10 | pub async fn command_server_start(&mut self, _args: Args) -> tg::Result<()> { 11 | self.start_server().await?; 12 | Ok(()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/cli/src/server/status.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | /// Get the server's health. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args {} 8 | 9 | impl Cli { 10 | pub async fn command_server_status(&mut self, _args: Args) -> tg::Result<()> { 11 | if self.client().await.is_ok() { 12 | println!("started"); 13 | } else { 14 | println!("stopped"); 15 | } 16 | Ok(()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/cli/src/server/stop.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | /// Stop the server. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args {} 8 | 9 | impl Cli { 10 | pub async fn command_server_stop(&mut self, _args: Args) -> tg::Result<()> { 11 | self.stop_server().await?; 12 | Ok(()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/cli/src/tag.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | pub mod delete; 5 | pub mod get; 6 | pub mod list; 7 | pub mod put; 8 | 9 | /// Manage tags. 10 | #[derive(Clone, Debug, clap::Args)] 11 | #[command( 12 | args_conflicts_with_subcommands = true, 13 | subcommand_negates_reqs = true, 14 | subcommand_precedence_over_arg = true 15 | )] 16 | #[group(skip)] 17 | pub struct Args { 18 | #[command(flatten)] 19 | pub args: crate::tag::put::Args, 20 | 21 | #[command(subcommand)] 22 | pub command: Option, 23 | } 24 | 25 | #[derive(Clone, Debug, clap::Subcommand)] 26 | pub enum Command { 27 | Delete(self::delete::Args), 28 | Get(self::get::Args), 29 | List(self::list::Args), 30 | Put(self::put::Args), 31 | } 32 | 33 | impl Cli { 34 | pub async fn command_tag(&mut self, args: Args) -> tg::Result<()> { 35 | match args.command.unwrap_or(Command::Put(args.args)) { 36 | Command::Delete(args) => { 37 | self.command_tag_delete(args).await?; 38 | }, 39 | Command::Get(args) => { 40 | self.command_tag_get(args).await?; 41 | }, 42 | Command::List(args) => { 43 | self.command_tag_list(args).await?; 44 | }, 45 | Command::Put(args) => { 46 | self.command_tag_put(args).await?; 47 | }, 48 | } 49 | Ok(()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/cli/src/tag/delete.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// Delete a tag. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(index = 1)] 9 | pub tag: tg::Tag, 10 | } 11 | 12 | impl Cli { 13 | pub async fn command_tag_delete(&mut self, args: Args) -> tg::Result<()> { 14 | let handle = self.handle().await?; 15 | handle.delete_tag(&args.tag).await?; 16 | Ok(()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/cli/src/tag/get.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// Get a tag. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(index = 1)] 9 | pub pattern: tg::tag::Pattern, 10 | } 11 | 12 | impl Cli { 13 | pub async fn command_tag_get(&mut self, args: Args) -> tg::Result<()> { 14 | let handle = self.handle().await?; 15 | let tag = handle.get_tag(&args.pattern).await?; 16 | let item = tag.item; 17 | println!("{item}"); 18 | Ok(()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/cli/src/tag/list.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client::{self as tg, prelude::*}; 3 | 4 | /// List tags. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | #[arg(index = 1)] 9 | pub pattern: tg::tag::Pattern, 10 | 11 | #[allow(clippy::option_option)] 12 | #[arg(short, long)] 13 | pub remote: Option>, 14 | 15 | #[arg(default_value = "false", long, action = clap::ArgAction::Set)] 16 | pub reverse: bool, 17 | } 18 | 19 | impl Cli { 20 | pub async fn command_tag_list(&mut self, args: Args) -> tg::Result<()> { 21 | let handle = self.handle().await?; 22 | 23 | // Get the remote. 24 | let remote = args 25 | .remote 26 | .map(|option| option.unwrap_or_else(|| "default".to_owned())); 27 | 28 | // List the tags. 29 | let arg = tg::tag::list::Arg { 30 | length: None, 31 | pattern: args.pattern, 32 | remote, 33 | reverse: args.reverse, 34 | }; 35 | let output = handle.list_tags(arg).await?; 36 | 37 | // Print the tags. 38 | for output in output.data { 39 | println!("{}", output.tag); 40 | } 41 | 42 | Ok(()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/cli/src/tangram.ascii: -------------------------------------------------------------------------------- 1 |     2 |     3 |     4 |     5 |      6 |      7 |      8 |      9 |       10 |       11 |       12 |       13 |      14 |      15 |      16 |      17 | -------------------------------------------------------------------------------- /packages/cli/src/tangram.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | pub mod update; 5 | 6 | /// Manage Tangram. 7 | #[derive(Clone, Debug, clap::Args)] 8 | #[group(skip)] 9 | pub struct Args { 10 | #[clap(subcommand)] 11 | pub command: Command, 12 | } 13 | 14 | #[derive(Clone, Debug, clap::Subcommand)] 15 | pub enum Command { 16 | Update(self::update::Args), 17 | } 18 | 19 | impl Cli { 20 | pub async fn command_tangram(&mut self, args: Args) -> tg::Result<()> { 21 | match args.command { 22 | Command::Update(args) => { 23 | self.command_tangram_update(args).await?; 24 | }, 25 | } 26 | Ok(()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/cli/src/tangram/update.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | /// Update Tangram to the latest version. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args {} 8 | 9 | impl Cli { 10 | pub async fn command_tangram_update(&mut self, _args: Args) -> tg::Result<()> { 11 | tokio::process::Command::new("/bin/sh") 12 | .args(["-c", "curl -sSL https://www.tangram.dev/install.sh | sh"]) 13 | .status() 14 | .await 15 | .map_err(|source| tg::error!(!source, "failed to run the installer"))?; 16 | Ok(()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/cli/src/tree.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use tangram_client as tg; 3 | 4 | /// Display a tree for a process or an object. 5 | #[derive(Clone, Debug, clap::Args)] 6 | #[group(skip)] 7 | pub struct Args { 8 | /// The maximum depth to render. 9 | #[arg(long)] 10 | pub depth: Option, 11 | 12 | /// If this flag is set, the package's lockfile will not be updated. 13 | #[arg(long, default_value = "false")] 14 | pub locked: bool, 15 | 16 | /// The reference to display a tree for. 17 | #[arg(index = 1)] 18 | pub reference: tg::Reference, 19 | } 20 | 21 | impl Cli { 22 | pub async fn command_tree(&mut self, args: Args) -> tg::Result<()> { 23 | let args = crate::view::Args { 24 | depth: args.depth, 25 | kind: crate::view::Kind::Inline, 26 | locked: args.locked, 27 | print: true, 28 | reference: args.reference, 29 | }; 30 | self.command_view(args).await?; 31 | Ok(()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/cli/src/update.rs: -------------------------------------------------------------------------------- 1 | use crate::Cli; 2 | use futures::TryStreamExt as _; 3 | use std::path::PathBuf; 4 | use tangram_client::{self as tg, prelude::*}; 5 | 6 | /// Update a package's lockfile. 7 | #[derive(Clone, Debug, clap::Args)] 8 | #[group(skip)] 9 | pub struct Args { 10 | #[arg(index = 1, default_value = ".")] 11 | pub path: PathBuf, 12 | } 13 | 14 | impl Cli { 15 | pub async fn command_update(&mut self, args: Args) -> tg::Result<()> { 16 | let handle = self.handle().await?; 17 | 18 | // Get the absolute path. 19 | let path = std::path::absolute(&args.path) 20 | .map_err(|source| tg::error!(!source, "failed to get the absolute path"))?; 21 | 22 | // Remove an existing lockfile. 23 | tokio::fs::remove_file(path.clone().join(tg::package::LOCKFILE_FILE_NAME)) 24 | .await 25 | .ok(); 26 | 27 | // Check in the package. 28 | let arg = tg::checkin::Arg { 29 | destructive: false, 30 | deterministic: false, 31 | ignore: true, 32 | locked: false, 33 | lockfile: true, 34 | path, 35 | }; 36 | let stream = handle.checkin(arg).await?; 37 | stream.map_ok(|_| ()).try_collect::<()>().await?; 38 | 39 | Ok(()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/cli/src/util.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use tangram_client as tg; 3 | 4 | pub fn infer_module_kind(path: impl AsRef) -> Option { 5 | let path = path.as_ref(); 6 | if path.ends_with(".d.ts") { 7 | Some(tg::module::Kind::Dts) 8 | } else if path 9 | .extension() 10 | .is_some_and(|extension| extension.eq_ignore_ascii_case("js")) 11 | { 12 | Some(tg::module::Kind::Js) 13 | } else if path 14 | .extension() 15 | .is_some_and(|extension| extension.eq_ignore_ascii_case("ts")) 16 | { 17 | Some(tg::module::Kind::Ts) 18 | } else { 19 | None 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/cli/tests/blob.rs: -------------------------------------------------------------------------------- 1 | use insta::assert_snapshot; 2 | use tangram_cli::{assert_success, test::test}; 3 | use tangram_temp::{self as temp, Temp}; 4 | 5 | const TG: &str = env!("CARGO_BIN_EXE_tangram"); 6 | 7 | #[tokio::test] 8 | async fn create_from_file() { 9 | test(TG, async move |context| { 10 | let server = context.spawn_server().await.unwrap(); 11 | 12 | let temp = Temp::new(); 13 | let artifact: temp::Artifact = temp::file!("hello, world!\n").into(); 14 | artifact.to_path(temp.path()).await.unwrap(); 15 | 16 | let mut file = tokio::fs::File::open(temp.path()).await.unwrap(); 17 | 18 | let mut child = server 19 | .tg() 20 | .arg("blob") 21 | .stdin(std::process::Stdio::piped()) 22 | .stdout(std::process::Stdio::piped()) 23 | .spawn() 24 | .unwrap(); 25 | let mut stdin = child.stdin.take().expect("Failed to get stdin"); 26 | tokio::io::copy(&mut file, &mut stdin).await.unwrap(); 27 | drop(stdin); 28 | let output = child.wait_with_output().await.unwrap(); 29 | assert_success!(output); 30 | 31 | let id = std::str::from_utf8(&output.stdout) 32 | .unwrap() 33 | .trim() 34 | .to_owned(); 35 | let output = server 36 | .tg() 37 | .arg("get") 38 | .arg(id) 39 | .arg("--format") 40 | .arg("tgvn") 41 | .arg("--pretty") 42 | .arg("true") 43 | .arg("--recursive") 44 | .output() 45 | .await 46 | .unwrap(); 47 | assert_success!(output); 48 | assert_snapshot!(std::str::from_utf8(&output.stdout).unwrap(), @r#"tg.blob("hello, world!\n")"#); 49 | }) 50 | .await; 51 | } 52 | -------------------------------------------------------------------------------- /packages/cli/tests/checksum.rs: -------------------------------------------------------------------------------- 1 | use insta::assert_snapshot; 2 | use tangram_cli::test::{assert_failure, assert_success, test}; 3 | 4 | const TG: &str = env!("CARGO_BIN_EXE_tangram"); 5 | 6 | #[tokio::test] 7 | async fn download_checksum_none() { 8 | test(TG, async move |context| { 9 | // Start the server. 10 | let server = context.spawn_server().await.unwrap(); 11 | 12 | // Download with checksum "none". 13 | let output = server 14 | .tg() 15 | .arg("download") 16 | .arg("https://example.com") 17 | .arg("--checksum") 18 | .arg("none") 19 | .output() 20 | .await 21 | .unwrap(); 22 | assert_failure!(output); 23 | 24 | // Download with the correct checksum. 25 | let output = server 26 | .tg() 27 | .arg("download") 28 | .arg("https://example.com") 29 | .arg("--checksum") 30 | .arg("sha256:ea8fac7c65fb589b0d53560f5251f74f9e9b243478dcb6b3ea79b5e36449c8d9") 31 | .output() 32 | .await 33 | .unwrap(); 34 | assert_success!(output); 35 | let stdout = std::str::from_utf8(&output.stdout).unwrap(); 36 | assert_snapshot!(stdout, @"blb_01fvba7brv0fdzcx0khtms8fm61wbeaf1a75f3td9c2mj6hrp9qr30"); 37 | }) 38 | .await; 39 | } 40 | -------------------------------------------------------------------------------- /packages/client/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rerun-if-env-changed=TANGRAM_CLI_COMMIT_HASH"); 3 | } 4 | -------------------------------------------------------------------------------- /packages/client/src/artifact.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | data::Artifact as Data, handle::Artifact as Handle, id::Id, kind::Kind, 3 | object::Artifact as Object, 4 | }; 5 | 6 | pub mod data; 7 | pub mod handle; 8 | pub mod id; 9 | pub mod kind; 10 | pub mod object; 11 | -------------------------------------------------------------------------------- /packages/client/src/artifact/kind.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | 3 | /// An artifact kind. 4 | #[derive(Clone, Copy, Debug)] 5 | pub enum Kind { 6 | Directory, 7 | File, 8 | Symlink, 9 | } 10 | 11 | impl std::fmt::Display for Kind { 12 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 13 | match self { 14 | Self::Directory => write!(f, "directory"), 15 | Self::File => write!(f, "file"), 16 | Self::Symlink => write!(f, "symlink"), 17 | } 18 | } 19 | } 20 | 21 | impl std::str::FromStr for Kind { 22 | type Err = tg::Error; 23 | 24 | fn from_str(s: &str) -> Result { 25 | match s { 26 | "directory" => Ok(Self::Directory), 27 | "file" => Ok(Self::File), 28 | "symlink" => Ok(Self::Symlink), 29 | _ => Err(tg::error!(%kind = s, "invalid kind")), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/client/src/artifact/object.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use std::sync::Arc; 3 | 4 | #[derive(Clone, Debug, derive_more::From, derive_more::TryUnwrap)] 5 | #[try_unwrap(ref)] 6 | pub enum Artifact { 7 | /// A directory. 8 | Directory(Arc), 9 | 10 | /// A file. 11 | File(Arc), 12 | 13 | /// A symlink. 14 | Symlink(Arc), 15 | } 16 | 17 | impl Artifact { 18 | #[must_use] 19 | pub fn children(&self) -> Vec { 20 | match self { 21 | Self::Directory(directory) => directory.children(), 22 | Self::File(file) => file.children(), 23 | Self::Symlink(symlink) => symlink.children(), 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/client/src/blob.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | data::Blob as Data, 3 | handle::Blob as Handle, 4 | id::Id, 5 | object::{Blob as Object, Child}, 6 | }; 7 | 8 | pub mod create; 9 | pub mod data; 10 | pub mod handle; 11 | pub mod id; 12 | pub mod object; 13 | pub mod read; 14 | -------------------------------------------------------------------------------- /packages/client/src/blob/create.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{Body, response::Ext as _}; 3 | use tokio::io::AsyncRead; 4 | 5 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 6 | pub struct Output { 7 | pub blob: tg::blob::Id, 8 | } 9 | 10 | impl tg::Client { 11 | pub async fn create_blob( 12 | &self, 13 | reader: impl AsyncRead + Send + 'static, 14 | ) -> tg::Result { 15 | let method = http::Method::POST; 16 | let uri = "/blobs"; 17 | let body = Body::with_reader(reader); 18 | let request = http::request::Builder::default() 19 | .method(method) 20 | .uri(uri) 21 | .body(body) 22 | .unwrap(); 23 | let response = self.send(request).await?; 24 | if !response.status().is_success() { 25 | let error = response.json().await?; 26 | return Err(error); 27 | } 28 | let output = response.json().await?; 29 | Ok(output) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/client/src/blob/id.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | 3 | #[derive( 4 | Clone, 5 | Debug, 6 | Eq, 7 | Hash, 8 | Ord, 9 | PartialEq, 10 | PartialOrd, 11 | derive_more::Display, 12 | derive_more::Into, 13 | serde::Deserialize, 14 | serde::Serialize, 15 | )] 16 | #[serde(into = "crate::Id", try_from = "crate::Id")] 17 | pub struct Id(pub(crate) crate::Id); 18 | 19 | impl Id { 20 | #[must_use] 21 | pub fn new(bytes: &[u8]) -> Self { 22 | Self(crate::Id::new_blake3(tg::id::Kind::Blob, bytes)) 23 | } 24 | } 25 | 26 | impl TryFrom for Id { 27 | type Error = tg::Error; 28 | 29 | fn try_from(value: crate::Id) -> tg::Result { 30 | if value.kind() != tg::id::Kind::Blob { 31 | return Err(tg::error!(%value, "invalid kind")); 32 | } 33 | Ok(Self(value)) 34 | } 35 | } 36 | 37 | impl std::str::FromStr for Id { 38 | type Err = tg::Error; 39 | 40 | fn from_str(s: &str) -> tg::Result { 41 | crate::Id::from_str(s)?.try_into() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/client/src/blob/object.rs: -------------------------------------------------------------------------------- 1 | use super::Data; 2 | use crate as tg; 3 | use bytes::Bytes; 4 | 5 | #[derive(Clone, Debug, derive_more::IsVariant)] 6 | pub enum Blob { 7 | Leaf(Leaf), 8 | Branch(Branch), 9 | } 10 | 11 | #[derive(Clone, Debug, Default)] 12 | pub struct Leaf { 13 | pub bytes: Bytes, 14 | } 15 | 16 | #[derive(Clone, Debug)] 17 | pub struct Branch { 18 | pub children: Vec, 19 | } 20 | 21 | #[derive(Clone, Debug)] 22 | pub struct Child { 23 | pub blob: tg::Blob, 24 | pub length: u64, 25 | } 26 | 27 | impl Blob { 28 | #[must_use] 29 | pub fn children(&self) -> Vec { 30 | match self { 31 | Self::Leaf(_) => vec![], 32 | Self::Branch(branch) => branch 33 | .children 34 | .iter() 35 | .map(|child| child.blob.clone().into()) 36 | .collect(), 37 | } 38 | } 39 | } 40 | 41 | impl TryFrom for Blob { 42 | type Error = tg::Error; 43 | 44 | fn try_from(data: Data) -> Result { 45 | match data { 46 | Data::Leaf(data) => Ok(Self::Leaf(Leaf { bytes: data.bytes })), 47 | Data::Branch(data) => { 48 | let children = data 49 | .children 50 | .into_iter() 51 | .map(|child| Child { 52 | blob: tg::Blob::with_id(child.blob), 53 | length: child.length, 54 | }) 55 | .collect(); 56 | Ok(Self::Branch(Branch { children })) 57 | }, 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/client/src/bytes.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use std::ops::Deref; 3 | 4 | #[derive(Clone, Debug, derive_more::From)] 5 | pub enum Cow<'a> { 6 | Borrowed(&'a [u8]), 7 | Owned(Bytes), 8 | } 9 | 10 | impl Cow<'_> { 11 | pub fn into_owned(self) -> Bytes { 12 | match self { 13 | Self::Borrowed(slice) => Bytes::copy_from_slice(slice), 14 | Self::Owned(bytes) => bytes, 15 | } 16 | } 17 | 18 | pub fn as_slice(&self) -> &[u8] { 19 | match self { 20 | Self::Borrowed(slice) => slice, 21 | Self::Owned(bytes) => bytes.as_ref(), 22 | } 23 | } 24 | } 25 | 26 | impl AsRef<[u8]> for Cow<'_> { 27 | fn as_ref(&self) -> &[u8] { 28 | self.as_slice() 29 | } 30 | } 31 | 32 | impl Deref for Cow<'_> { 33 | type Target = [u8]; 34 | 35 | fn deref(&self) -> &Self::Target { 36 | self.as_slice() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/client/src/check.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 5 | pub struct Arg { 6 | pub package: tg::module::Data, 7 | 8 | #[serde(default, skip_serializing_if = "Option::is_none")] 9 | pub remote: Option, 10 | } 11 | 12 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 13 | pub struct Output { 14 | pub diagnostics: Vec, 15 | } 16 | 17 | impl tg::Client { 18 | pub async fn check(&self, arg: Arg) -> tg::Result { 19 | let method = http::Method::POST; 20 | let uri = "/check"; 21 | let request = http::request::Builder::default() 22 | .method(method) 23 | .uri(uri) 24 | .json(arg) 25 | .unwrap(); 26 | let response = self.send(request).await?; 27 | if !response.status().is_success() { 28 | let error = response.json().await?; 29 | return Err(error); 30 | } 31 | let output = response.json().await?; 32 | Ok(output) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/client/src/clean.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use futures::{Stream, TryStreamExt as _, future}; 3 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 4 | 5 | impl tg::Client { 6 | pub async fn clean( 7 | &self, 8 | ) -> tg::Result>> + Send + 'static> { 9 | let method = http::Method::POST; 10 | let uri = "/clean"; 11 | let request = http::request::Builder::default() 12 | .method(method) 13 | .uri(uri) 14 | .header(http::header::ACCEPT, mime::TEXT_EVENT_STREAM.to_string()) 15 | .empty() 16 | .unwrap(); 17 | let response = self.send(request).await?; 18 | if !response.status().is_success() { 19 | let error = response.json().await?; 20 | return Err(error); 21 | } 22 | let content_type = response 23 | .parse_header::(http::header::CONTENT_TYPE) 24 | .transpose()?; 25 | if !matches!( 26 | content_type 27 | .as_ref() 28 | .map(|content_type| (content_type.type_(), content_type.subtype())), 29 | Some((mime::TEXT, mime::EVENT_STREAM)), 30 | ) { 31 | return Err(tg::error!(?content_type, "invalid content type")); 32 | } 33 | let stream = response 34 | .sse() 35 | .map_err(|source| tg::error!(!source, "failed to read an event")) 36 | .and_then(|event| { 37 | future::ready( 38 | if event.event.as_deref().is_some_and(|event| event == "error") { 39 | match event.try_into() { 40 | Ok(error) | Err(error) => Err(error), 41 | } 42 | } else { 43 | event.try_into() 44 | }, 45 | ) 46 | }); 47 | Ok(stream) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/client/src/command.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | builder::Builder, 3 | data::Command as Data, 4 | handle::Command as Handle, 5 | id::Id, 6 | object::{ 7 | ArtifactExecutable, Command as Object, Executable, ModuleExecutable, Mount, PathExecutable, 8 | }, 9 | }; 10 | 11 | pub mod builder; 12 | pub mod data; 13 | pub mod handle; 14 | pub mod id; 15 | pub mod object; 16 | -------------------------------------------------------------------------------- /packages/client/src/command/id.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | 3 | #[derive( 4 | Clone, 5 | Debug, 6 | Eq, 7 | Hash, 8 | Ord, 9 | PartialEq, 10 | PartialOrd, 11 | derive_more::Display, 12 | derive_more::Into, 13 | serde::Deserialize, 14 | serde::Serialize, 15 | )] 16 | #[serde(into = "crate::Id", try_from = "crate::Id")] 17 | pub struct Id(pub(crate) crate::Id); 18 | 19 | impl Id { 20 | #[must_use] 21 | pub fn new(bytes: &[u8]) -> Self { 22 | Self(crate::Id::new_blake3(tg::id::Kind::Command, bytes)) 23 | } 24 | } 25 | 26 | impl TryFrom for Id { 27 | type Error = tg::Error; 28 | 29 | fn try_from(value: crate::Id) -> tg::Result { 30 | if value.kind() != tg::id::Kind::Command { 31 | return Err(tg::error!(%value, "invalid kind")); 32 | } 33 | Ok(Self(value)) 34 | } 35 | } 36 | 37 | impl std::str::FromStr for Id { 38 | type Err = tg::Error; 39 | 40 | fn from_str(s: &str) -> tg::Result { 41 | crate::Id::from_str(s)?.try_into() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/client/src/directory.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | builder::Builder, data::Directory as Data, handle::Directory as Handle, id::Id, 3 | object::Directory as Object, 4 | }; 5 | 6 | pub mod builder; 7 | pub mod data; 8 | pub mod handle; 9 | pub mod id; 10 | pub mod object; 11 | -------------------------------------------------------------------------------- /packages/client/src/directory/data.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use bytes::Bytes; 3 | use itertools::Itertools as _; 4 | use std::collections::{BTreeMap, BTreeSet}; 5 | 6 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 7 | #[serde(untagged)] 8 | pub enum Directory { 9 | Graph { 10 | graph: tg::graph::Id, 11 | node: usize, 12 | }, 13 | Normal { 14 | entries: BTreeMap, 15 | }, 16 | } 17 | 18 | impl Directory { 19 | pub fn serialize(&self) -> tg::Result { 20 | serde_json::to_vec(self) 21 | .map(Into::into) 22 | .map_err(|source| tg::error!(!source, "failed to serialize the data")) 23 | } 24 | 25 | pub fn deserialize<'a>(bytes: impl Into>) -> tg::Result { 26 | serde_json::from_reader(bytes.into().as_ref()) 27 | .map_err(|source| tg::error!(!source, "failed to deserialize the data")) 28 | } 29 | 30 | #[must_use] 31 | pub fn children(&self) -> BTreeSet { 32 | match self { 33 | Self::Graph { graph, .. } => std::iter::once(graph.clone()).map_into().collect(), 34 | Self::Normal { entries } => entries.values().cloned().map(Into::into).collect(), 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/client/src/directory/id.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | 3 | #[derive( 4 | Clone, 5 | Debug, 6 | Eq, 7 | Hash, 8 | Ord, 9 | PartialEq, 10 | PartialOrd, 11 | derive_more::Display, 12 | derive_more::Into, 13 | serde::Deserialize, 14 | serde::Serialize, 15 | )] 16 | #[serde(into = "crate::Id", try_from = "crate::Id")] 17 | pub struct Id(pub(crate) crate::Id); 18 | 19 | impl Id { 20 | #[must_use] 21 | pub fn new(bytes: &[u8]) -> Self { 22 | Self(crate::Id::new_blake3(tg::id::Kind::Directory, bytes)) 23 | } 24 | } 25 | 26 | impl TryFrom for Id { 27 | type Error = tg::Error; 28 | 29 | fn try_from(value: crate::Id) -> tg::Result { 30 | if value.kind() != tg::id::Kind::Directory { 31 | return Err(tg::error!(%value, "invalid kind")); 32 | } 33 | Ok(Self(value)) 34 | } 35 | } 36 | 37 | impl std::str::FromStr for Id { 38 | type Err = tg::Error; 39 | 40 | fn from_str(s: &str) -> tg::Result { 41 | crate::Id::from_str(s)?.try_into() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/client/src/directory/object.rs: -------------------------------------------------------------------------------- 1 | use super::Data; 2 | use crate as tg; 3 | use itertools::Itertools as _; 4 | use std::collections::BTreeMap; 5 | 6 | #[derive(Clone, Debug)] 7 | pub enum Directory { 8 | Graph { 9 | graph: tg::Graph, 10 | node: usize, 11 | }, 12 | Normal { 13 | entries: BTreeMap, 14 | }, 15 | } 16 | 17 | impl Directory { 18 | #[must_use] 19 | pub fn children(&self) -> Vec { 20 | match self { 21 | Self::Graph { graph, .. } => std::iter::once(graph.clone()).map_into().collect(), 22 | Self::Normal { entries } => entries.values().cloned().map(Into::into).collect(), 23 | } 24 | } 25 | } 26 | 27 | impl TryFrom for Directory { 28 | type Error = tg::Error; 29 | 30 | fn try_from(data: Data) -> Result { 31 | match data { 32 | Data::Graph { graph, node } => { 33 | let graph = tg::Graph::with_id(graph); 34 | Ok(Self::Graph { graph, node }) 35 | }, 36 | Data::Normal { entries } => { 37 | let entries = entries 38 | .into_iter() 39 | .map(|(name, id)| (name, tg::Artifact::with_id(id))) 40 | .collect(); 41 | Ok(Self::Normal { entries }) 42 | }, 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/client/src/document.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 5 | pub struct Arg { 6 | pub module: tg::module::Data, 7 | 8 | #[serde(default, skip_serializing_if = "Option::is_none")] 9 | pub remote: Option, 10 | } 11 | 12 | impl tg::Client { 13 | pub async fn document(&self, arg: Arg) -> tg::Result { 14 | let method = http::Method::POST; 15 | let uri = "/document"; 16 | let request = http::request::Builder::default() 17 | .method(method) 18 | .uri(uri) 19 | .json(arg) 20 | .unwrap(); 21 | let response = self.send(request).await?; 22 | if !response.status().is_success() { 23 | let error = response.json().await?; 24 | return Err(error); 25 | } 26 | let output = response.json().await?; 27 | Ok(output) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/client/src/file.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | builder::Builder, data::File as Data, handle::File as Handle, id::Id, object::File as Object, 3 | }; 4 | 5 | pub mod builder; 6 | pub mod data; 7 | pub mod handle; 8 | pub mod id; 9 | pub mod object; 10 | 11 | /// The extended attribute name used to store lockfile data. 12 | pub const XATTR_LOCK_NAME: &str = "user.tangram.lock"; 13 | -------------------------------------------------------------------------------- /packages/client/src/file/builder.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use std::collections::BTreeMap; 3 | 4 | pub struct Builder { 5 | contents: tg::Blob, 6 | dependencies: BTreeMap>, 7 | executable: bool, 8 | } 9 | 10 | impl Builder { 11 | #[must_use] 12 | pub fn new(contents: impl Into) -> Self { 13 | Self { 14 | contents: contents.into(), 15 | dependencies: BTreeMap::new(), 16 | executable: false, 17 | } 18 | } 19 | 20 | #[must_use] 21 | pub fn contents(mut self, contents: impl Into) -> Self { 22 | self.contents = contents.into(); 23 | self 24 | } 25 | 26 | #[must_use] 27 | pub fn dependencies( 28 | mut self, 29 | dependencies: impl IntoIterator)>, 30 | ) -> Self { 31 | self.dependencies = dependencies.into_iter().collect(); 32 | self 33 | } 34 | 35 | #[must_use] 36 | pub fn executable(mut self, executable: impl Into) -> Self { 37 | self.executable = executable.into(); 38 | self 39 | } 40 | 41 | #[must_use] 42 | pub fn build(self) -> tg::File { 43 | tg::File::with_object(tg::file::Object::Normal { 44 | contents: self.contents, 45 | dependencies: self.dependencies, 46 | executable: self.executable, 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/client/src/file/id.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | 3 | #[derive( 4 | Clone, 5 | Debug, 6 | Eq, 7 | Hash, 8 | Ord, 9 | PartialEq, 10 | PartialOrd, 11 | derive_more::Display, 12 | derive_more::Into, 13 | serde::Deserialize, 14 | serde::Serialize, 15 | )] 16 | #[serde(into = "crate::Id", try_from = "crate::Id")] 17 | pub struct Id(pub(crate) crate::Id); 18 | 19 | impl Id { 20 | #[must_use] 21 | pub fn new(bytes: &[u8]) -> Self { 22 | Self(crate::Id::new_blake3(tg::id::Kind::File, bytes)) 23 | } 24 | } 25 | 26 | impl TryFrom for Id { 27 | type Error = tg::Error; 28 | 29 | fn try_from(value: crate::Id) -> tg::Result { 30 | if value.kind() != tg::id::Kind::File { 31 | return Err(tg::error!(%value, "invalid kind")); 32 | } 33 | Ok(Self(value)) 34 | } 35 | } 36 | 37 | impl std::str::FromStr for Id { 38 | type Err = tg::Error; 39 | 40 | fn from_str(s: &str) -> tg::Result { 41 | crate::Id::from_str(s)?.try_into() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/client/src/format.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 5 | pub struct Arg { 6 | pub module: tg::module::Data, 7 | } 8 | 9 | impl tg::Client { 10 | pub async fn format(&self, arg: tg::format::Arg) -> tg::Result<()> { 11 | let method = http::Method::POST; 12 | let uri = "/format"; 13 | let request = http::request::Builder::default() 14 | .method(method) 15 | .uri(uri) 16 | .json(arg) 17 | .unwrap(); 18 | let response = self.send(request).await?; 19 | if !response.status().is_success() { 20 | let error = response.json().await?; 21 | return Err(error); 22 | } 23 | Ok(()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/client/src/graph.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | data::Graph as Data, 3 | handle::Graph as Handle, 4 | id::Id, 5 | object::{Graph as Object, Node}, 6 | }; 7 | 8 | pub mod data; 9 | pub mod handle; 10 | pub mod id; 11 | pub mod object; 12 | -------------------------------------------------------------------------------- /packages/client/src/graph/id.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | 3 | #[derive( 4 | Clone, 5 | Debug, 6 | Eq, 7 | Hash, 8 | Ord, 9 | PartialEq, 10 | PartialOrd, 11 | derive_more::Display, 12 | derive_more::Into, 13 | serde::Deserialize, 14 | serde::Serialize, 15 | )] 16 | #[serde(into = "crate::Id", try_from = "crate::Id")] 17 | pub struct Id(pub(crate) crate::Id); 18 | 19 | impl Id { 20 | #[must_use] 21 | pub fn new(bytes: &[u8]) -> Self { 22 | Self(crate::Id::new_blake3(tg::id::Kind::Graph, bytes)) 23 | } 24 | } 25 | 26 | impl TryFrom for Id { 27 | type Error = tg::Error; 28 | 29 | fn try_from(value: crate::Id) -> tg::Result { 30 | if value.kind() != tg::id::Kind::Graph { 31 | return Err(tg::error!(%value, "invalid kind")); 32 | } 33 | Ok(Self(value)) 34 | } 35 | } 36 | 37 | impl std::str::FromStr for Id { 38 | type Err = tg::Error; 39 | 40 | fn from_str(s: &str) -> tg::Result { 41 | crate::Id::from_str(s)?.try_into() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/client/src/health.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 5 | pub struct Health { 6 | pub database: Option, 7 | pub diagnostics: Vec, 8 | pub file_descriptor_semaphore: Option, 9 | pub processes: Option, 10 | pub version: Option, 11 | } 12 | 13 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 14 | pub struct Processes { 15 | pub created: u64, 16 | pub dequeued: u64, 17 | pub enqueued: u64, 18 | pub started: u64, 19 | } 20 | 21 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 22 | pub struct Database { 23 | pub available_connections: u64, 24 | } 25 | 26 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 27 | pub struct FileDescriptorSemaphore { 28 | pub available_permits: u64, 29 | } 30 | 31 | impl tg::Client { 32 | pub async fn health(&self) -> tg::Result { 33 | let method = http::Method::GET; 34 | let uri = "/health"; 35 | let request = http::request::Builder::default() 36 | .method(method) 37 | .uri(uri) 38 | .empty() 39 | .unwrap(); 40 | let response = self.send(request).await?; 41 | if !response.status().is_success() { 42 | let error = response.json().await?; 43 | return Err(error); 44 | } 45 | let output = response.json().await?; 46 | Ok(output) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/client/src/index.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use futures::{Stream, TryStreamExt as _, future}; 3 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 4 | 5 | impl tg::Client { 6 | pub async fn index( 7 | &self, 8 | ) -> tg::Result>> + Send + 'static> { 9 | let method = http::Method::POST; 10 | let uri = "/index"; 11 | let request = http::request::Builder::default() 12 | .method(method) 13 | .uri(uri) 14 | .header(http::header::ACCEPT, mime::TEXT_EVENT_STREAM.to_string()) 15 | .empty() 16 | .unwrap(); 17 | let response = self.send(request).await?; 18 | if !response.status().is_success() { 19 | let error = response.json().await?; 20 | return Err(error); 21 | } 22 | let content_type = response 23 | .parse_header::(http::header::CONTENT_TYPE) 24 | .transpose()?; 25 | if !matches!( 26 | content_type 27 | .as_ref() 28 | .map(|content_type| (content_type.type_(), content_type.subtype())), 29 | Some((mime::TEXT, mime::EVENT_STREAM)), 30 | ) { 31 | return Err(tg::error!(?content_type, "invalid content type")); 32 | } 33 | let stream = response 34 | .sse() 35 | .map_err(|source| tg::error!(!source, "failed to read an event")) 36 | .and_then(|event| { 37 | future::ready( 38 | if event.event.as_deref().is_some_and(|event| event == "error") { 39 | match event.try_into() { 40 | Ok(error) | Err(error) => Err(error), 41 | } 42 | } else { 43 | event.try_into() 44 | }, 45 | ) 46 | }); 47 | Ok(stream) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/client/src/location.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | 3 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 4 | pub struct Location { 5 | pub module: tg::module::Data, 6 | pub range: tg::Range, 7 | } 8 | 9 | impl std::fmt::Display for Location { 10 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 11 | write!(f, "{}", self.module)?; 12 | let line = self.range.start.line + 1; 13 | let character = self.range.start.character + 1; 14 | write!(f, ":{line}:{character}")?; 15 | Ok(()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/client/src/object.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | data::Object as Data, handle::Object as Handle, id::Id, kind::Kind, metadata::Metadata, 3 | object::Object, state::State, 4 | }; 5 | 6 | pub mod data; 7 | pub mod get; 8 | pub mod handle; 9 | pub mod id; 10 | pub mod kind; 11 | pub mod metadata; 12 | #[allow(clippy::module_inception)] 13 | pub mod object; 14 | pub mod put; 15 | pub mod state; 16 | pub mod touch; 17 | -------------------------------------------------------------------------------- /packages/client/src/object/get.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use bytes::Bytes; 3 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 4 | 5 | #[derive(Clone, Debug)] 6 | pub struct Output { 7 | pub bytes: Bytes, 8 | } 9 | 10 | impl tg::Client { 11 | pub async fn try_get_object( 12 | &self, 13 | id: &tg::object::Id, 14 | ) -> tg::Result> { 15 | let method = http::Method::GET; 16 | let uri = format!("/objects/{id}"); 17 | let request = http::request::Builder::default() 18 | .method(method) 19 | .uri(uri) 20 | .empty() 21 | .unwrap(); 22 | let response = self.send(request).await?; 23 | if response.status() == http::StatusCode::NOT_FOUND { 24 | return Ok(None); 25 | } 26 | if !response.status().is_success() { 27 | let error = response.json().await?; 28 | return Err(error); 29 | } 30 | let bytes = response.bytes().await?; 31 | let output = tg::object::get::Output { bytes }; 32 | Ok(Some(output)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/client/src/object/kind.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | 3 | #[derive(Clone, Copy, Debug)] 4 | pub enum Kind { 5 | Blob, 6 | Directory, 7 | File, 8 | Symlink, 9 | Graph, 10 | Command, 11 | } 12 | 13 | impl std::fmt::Display for Kind { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | write!(f, "{}", tg::id::Kind::from(*self)) 16 | } 17 | } 18 | 19 | impl std::str::FromStr for Kind { 20 | type Err = tg::Error; 21 | 22 | fn from_str(s: &str) -> tg::Result { 23 | tg::id::Kind::from_str(s)?.try_into() 24 | } 25 | } 26 | 27 | impl From for tg::id::Kind { 28 | fn from(value: Kind) -> Self { 29 | match value { 30 | Kind::Blob => Self::Blob, 31 | Kind::Directory => Self::Directory, 32 | Kind::File => Self::File, 33 | Kind::Symlink => Self::Symlink, 34 | Kind::Graph => Self::Graph, 35 | Kind::Command => Self::Command, 36 | } 37 | } 38 | } 39 | 40 | impl TryFrom for Kind { 41 | type Error = tg::Error; 42 | 43 | fn try_from(value: tg::id::Kind) -> tg::Result { 44 | match value { 45 | tg::id::Kind::Blob => Ok(Self::Blob), 46 | tg::id::Kind::Directory => Ok(Self::Directory), 47 | tg::id::Kind::File => Ok(Self::File), 48 | tg::id::Kind::Symlink => Ok(Self::Symlink), 49 | tg::id::Kind::Graph => Ok(Self::Graph), 50 | tg::id::Kind::Command => Ok(Self::Command), 51 | kind => Err(tg::error!(%kind, "invalid kind")), 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/client/src/object/metadata.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive( 5 | Clone, Debug, Default, PartialEq, PartialOrd, Eq, Hash, serde::Deserialize, serde::Serialize, 6 | )] 7 | pub struct Metadata { 8 | pub count: Option, 9 | pub depth: Option, 10 | pub weight: Option, 11 | } 12 | 13 | impl tg::Client { 14 | pub async fn try_get_object_metadata( 15 | &self, 16 | id: &tg::object::Id, 17 | ) -> tg::Result> { 18 | let method = http::Method::GET; 19 | let uri = format!("/objects/{id}/metadata"); 20 | let request = http::request::Builder::default() 21 | .method(method) 22 | .uri(uri) 23 | .empty() 24 | .unwrap(); 25 | let response = self.send(request).await?; 26 | if response.status() == http::StatusCode::NOT_FOUND { 27 | return Ok(None); 28 | } 29 | if !response.status().is_success() { 30 | let error = response.json().await?; 31 | return Err(error); 32 | } 33 | let metadata = response.json().await?; 34 | Ok(Some(metadata)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/client/src/object/object.rs: -------------------------------------------------------------------------------- 1 | use super::Data; 2 | use crate as tg; 3 | use std::sync::Arc; 4 | 5 | #[derive(Clone, Debug, derive_more::From, derive_more::TryInto, derive_more::TryUnwrap)] 6 | #[try_unwrap(ref)] 7 | pub enum Object { 8 | Blob(Arc), 9 | Directory(Arc), 10 | File(Arc), 11 | Symlink(Arc), 12 | Graph(Arc), 13 | Command(Arc), 14 | } 15 | 16 | impl Object { 17 | #[must_use] 18 | pub fn children(&self) -> Vec { 19 | match self { 20 | Self::Blob(blob) => blob.children(), 21 | Self::Directory(directory) => directory.children(), 22 | Self::File(file) => file.children(), 23 | Self::Symlink(symlink) => symlink.children(), 24 | Self::Graph(graph) => graph.children(), 25 | Self::Command(command) => command.children(), 26 | } 27 | } 28 | } 29 | 30 | impl TryFrom for Object { 31 | type Error = tg::Error; 32 | 33 | fn try_from(data: Data) -> Result { 34 | Ok(match data { 35 | Data::Blob(data) => Self::Blob(Arc::new(tg::blob::Object::try_from(data)?)), 36 | Data::Directory(data) => { 37 | Self::Directory(Arc::new(tg::directory::Object::try_from(data)?)) 38 | }, 39 | Data::File(data) => Self::File(Arc::new(tg::file::Object::try_from(data)?)), 40 | Data::Symlink(data) => Self::Symlink(Arc::new(tg::symlink::Object::try_from(data)?)), 41 | Data::Graph(data) => Self::Graph(Arc::new(tg::graph::Object::try_from(data)?)), 42 | Data::Command(data) => Self::Command(Arc::new(tg::command::Object::try_from(data)?)), 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/client/src/object/put.rs: -------------------------------------------------------------------------------- 1 | use crate::{self as tg, util::serde::BytesBase64}; 2 | use bytes::Bytes; 3 | use serde_with::serde_as; 4 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 5 | 6 | #[serde_as] 7 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 8 | pub struct Arg { 9 | #[serde_as(as = "BytesBase64")] 10 | pub bytes: Bytes, 11 | } 12 | 13 | impl tg::Client { 14 | pub async fn put_object( 15 | &self, 16 | id: &tg::object::Id, 17 | arg: tg::object::put::Arg, 18 | ) -> tg::Result<()> { 19 | let method = http::Method::PUT; 20 | let uri = format!("/objects/{id}"); 21 | let request = http::request::Builder::default() 22 | .method(method) 23 | .uri(uri) 24 | .bytes(arg.bytes.clone()) 25 | .unwrap(); 26 | let response = self.send(request).await?; 27 | if !response.status().is_success() { 28 | let error = response.json().await?; 29 | return Err(error); 30 | } 31 | Ok(()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/client/src/object/state.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | #[derive(Debug)] 4 | pub struct State { 5 | pub id: Option, 6 | pub object: Option>, 7 | } 8 | 9 | impl State { 10 | #[must_use] 11 | pub fn new(id: Option, object: Option>>) -> Self { 12 | assert!(id.is_some() || object.is_some()); 13 | let object = object.map(Into::into); 14 | Self { id, object } 15 | } 16 | 17 | #[must_use] 18 | pub fn with_id(id: I) -> Self { 19 | Self { 20 | id: Some(id), 21 | object: None, 22 | } 23 | } 24 | 25 | #[must_use] 26 | pub fn with_object(object: impl Into>) -> Self { 27 | Self { 28 | id: None, 29 | object: Some(object.into()), 30 | } 31 | } 32 | 33 | #[must_use] 34 | pub fn id(&self) -> Option<&I> { 35 | self.id.as_ref() 36 | } 37 | 38 | #[must_use] 39 | pub fn object(&self) -> Option<&Arc> { 40 | self.object.as_ref() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/client/src/object/touch.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] 5 | pub struct Arg { 6 | #[serde(default, skip_serializing_if = "Option::is_none")] 7 | pub remote: Option, 8 | } 9 | 10 | impl tg::Client { 11 | pub async fn touch_object( 12 | &self, 13 | id: &tg::object::Id, 14 | arg: tg::object::touch::Arg, 15 | ) -> tg::Result<()> { 16 | let method = http::Method::POST; 17 | let uri = format!("/processes/{id}/touch"); 18 | let request = http::request::Builder::default() 19 | .method(method) 20 | .uri(uri) 21 | .json(arg) 22 | .unwrap(); 23 | let response = self.send(request).await?; 24 | if !response.status().is_success() { 25 | let error = response.json().await?; 26 | return Err(error); 27 | } 28 | Ok(()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/client/src/pipe.rs: -------------------------------------------------------------------------------- 1 | use crate::util::serde::BytesBase64; 2 | use bytes::Bytes; 3 | use serde_with::serde_as; 4 | 5 | pub mod close; 6 | pub mod create; 7 | pub mod id; 8 | pub mod read; 9 | pub mod write; 10 | 11 | pub use self::id::Id; 12 | 13 | #[serde_as] 14 | #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] 15 | #[serde(rename_all = "snake_case", tag = "kind", content = "value")] 16 | pub enum Event { 17 | Chunk(#[serde_as(as = "BytesBase64")] Bytes), 18 | End, 19 | } 20 | -------------------------------------------------------------------------------- /packages/client/src/pipe/close.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Default, Clone, Debug, serde::Deserialize, serde::Serialize)] 5 | pub struct Arg { 6 | #[serde(default, skip_serializing_if = "Option::is_none")] 7 | pub remote: Option, 8 | } 9 | 10 | impl tg::Client { 11 | pub async fn close_pipe(&self, id: &tg::pipe::Id, arg: Arg) -> tg::Result<()> { 12 | let method = http::Method::POST; 13 | let query = serde_urlencoded::to_string(&arg).unwrap(); 14 | let uri = format!("/pipes/{id}/close?{query}"); 15 | let request = http::request::Builder::default() 16 | .method(method) 17 | .uri(uri) 18 | .empty() 19 | .unwrap(); 20 | let response = self.send(request).await?; 21 | if !response.status().is_success() { 22 | let error = response.json().await?; 23 | return Err(error); 24 | } 25 | Ok(()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/client/src/pipe/create.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Default, Clone, Debug, serde::Deserialize, serde::Serialize)] 5 | pub struct Arg { 6 | #[serde(default, skip_serializing_if = "Option::is_none")] 7 | pub remote: Option, 8 | } 9 | 10 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 11 | pub struct Output { 12 | pub id: tg::pipe::Id, 13 | } 14 | 15 | impl tg::Client { 16 | pub async fn create_pipe(&self, arg: Arg) -> tg::Result { 17 | let method = http::Method::POST; 18 | let uri = "/pipes"; 19 | let request = http::request::Builder::default() 20 | .method(method) 21 | .uri(uri) 22 | .json(arg) 23 | .unwrap(); 24 | let response = self.send(request).await?; 25 | if !response.status().is_success() { 26 | let error = response.json().await?; 27 | return Err(error); 28 | } 29 | let output = response.json().await?; 30 | Ok(output) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/client/src/pipe/id.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | 3 | #[derive( 4 | Clone, 5 | Debug, 6 | Eq, 7 | Hash, 8 | Ord, 9 | PartialEq, 10 | PartialOrd, 11 | derive_more::Display, 12 | serde::Deserialize, 13 | serde::Serialize, 14 | )] 15 | #[serde(into = "crate::Id", try_from = "crate::Id")] 16 | pub struct Id(pub(crate) crate::Id); 17 | 18 | impl Id { 19 | #[allow(clippy::new_without_default)] 20 | #[must_use] 21 | pub fn new() -> Self { 22 | Self(crate::Id::new_uuidv7(tg::id::Kind::Pipe)) 23 | } 24 | } 25 | 26 | impl From for crate::Id { 27 | fn from(value: Id) -> Self { 28 | value.0 29 | } 30 | } 31 | 32 | impl TryFrom for Id { 33 | type Error = tg::Error; 34 | 35 | fn try_from(value: crate::Id) -> tg::Result { 36 | if value.kind() != tg::id::Kind::Pipe { 37 | return Err(tg::error!(%value, "invalid kind")); 38 | } 39 | Ok(Self(value)) 40 | } 41 | } 42 | 43 | impl std::str::FromStr for Id { 44 | type Err = tg::Error; 45 | 46 | fn from_str(s: &str) -> tg::Result { 47 | crate::Id::from_str(s)?.try_into() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/client/src/process/children.rs: -------------------------------------------------------------------------------- 1 | pub mod get; 2 | -------------------------------------------------------------------------------- /packages/client/src/process/finish.rs: -------------------------------------------------------------------------------- 1 | use crate::{self as tg, util::serde::is_false}; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 5 | pub struct Arg { 6 | #[serde(default, skip_serializing_if = "Option::is_none")] 7 | pub checksum: Option, 8 | 9 | #[serde(default, skip_serializing_if = "Option::is_none")] 10 | pub error: Option, 11 | 12 | pub exit: u8, 13 | 14 | #[serde(default, skip_serializing_if = "is_false")] 15 | pub force: bool, 16 | 17 | #[serde(default, skip_serializing_if = "Option::is_none")] 18 | pub output: Option, 19 | 20 | #[serde(default, skip_serializing_if = "Option::is_none")] 21 | pub remote: Option, 22 | } 23 | 24 | impl tg::Process { 25 | pub async fn finish(&self, handle: &H, arg: tg::process::finish::Arg) -> tg::Result<()> 26 | where 27 | H: tg::Handle, 28 | { 29 | let id = self.id(); 30 | handle.finish_process(id, arg).await?; 31 | Ok(()) 32 | } 33 | } 34 | 35 | impl tg::Client { 36 | pub async fn finish_process( 37 | &self, 38 | id: &tg::process::Id, 39 | arg: tg::process::finish::Arg, 40 | ) -> tg::Result<()> { 41 | let method = http::Method::POST; 42 | let uri = format!("/processes/{id}/finish"); 43 | let request = http::request::Builder::default() 44 | .method(method) 45 | .uri(uri) 46 | .json(arg) 47 | .unwrap(); 48 | let response = self.send(request).await?; 49 | if !response.status().is_success() { 50 | let error = response.json().await?; 51 | return Err(error); 52 | } 53 | Ok(()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/client/src/process/get.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 5 | pub struct Output { 6 | #[serde(flatten)] 7 | pub data: tg::process::Data, 8 | } 9 | 10 | impl Output { 11 | pub fn objects(&self) -> Vec { 12 | self.data.objects() 13 | } 14 | } 15 | 16 | impl tg::Client { 17 | pub async fn try_get_process( 18 | &self, 19 | id: &tg::process::Id, 20 | ) -> tg::Result> { 21 | let method = http::Method::GET; 22 | let uri = format!("/processes/{id}"); 23 | let request = http::request::Builder::default() 24 | .method(method) 25 | .uri(uri) 26 | .empty() 27 | .unwrap(); 28 | let response = self.send(request).await?; 29 | if response.status() == http::StatusCode::NOT_FOUND { 30 | return Ok(None); 31 | } 32 | if !response.status().is_success() { 33 | let error = response.json().await?; 34 | return Err(error); 35 | } 36 | let output = response.json().await?; 37 | Ok(Some(output)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/client/src/process/heartbeat.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] 5 | pub struct Arg { 6 | #[serde(default, skip_serializing_if = "Option::is_none")] 7 | pub remote: Option, 8 | } 9 | 10 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 11 | pub struct Output { 12 | pub status: tg::process::Status, 13 | } 14 | 15 | impl tg::Process { 16 | pub async fn heartbeat( 17 | &self, 18 | handle: &H, 19 | arg: tg::process::heartbeat::Arg, 20 | ) -> tg::Result 21 | where 22 | H: tg::Handle, 23 | { 24 | handle.heartbeat_process(self.id(), arg).await 25 | } 26 | } 27 | 28 | impl tg::Client { 29 | pub async fn heartbeat_process( 30 | &self, 31 | id: &tg::process::Id, 32 | arg: tg::process::heartbeat::Arg, 33 | ) -> tg::Result { 34 | let method = http::Method::POST; 35 | let uri = format!("/processes/{id}/heartbeat"); 36 | let request = http::request::Builder::default() 37 | .method(method) 38 | .uri(uri) 39 | .json(arg) 40 | .unwrap(); 41 | let response = self.send(request).await?; 42 | if !response.status().is_success() { 43 | let error = response.json().await?; 44 | return Err(error); 45 | } 46 | let output = response.json().await?; 47 | Ok(output) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/client/src/process/id.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | 3 | #[derive( 4 | Clone, 5 | Debug, 6 | Eq, 7 | Hash, 8 | Ord, 9 | PartialEq, 10 | PartialOrd, 11 | derive_more::Display, 12 | serde::Deserialize, 13 | serde::Serialize, 14 | )] 15 | #[serde(into = "crate::Id", try_from = "crate::Id")] 16 | pub struct Id(pub(crate) crate::Id); 17 | 18 | impl Id { 19 | #[allow(clippy::new_without_default)] 20 | #[must_use] 21 | pub fn new() -> Self { 22 | Self(crate::Id::new_uuidv7(tg::id::Kind::Process)) 23 | } 24 | 25 | #[must_use] 26 | pub fn to_bytes(&self) -> Vec { 27 | self.as_id().to_bytes() 28 | } 29 | 30 | pub fn from_slice(bytes: &[u8]) -> tg::Result { 31 | tg::Id::from_reader(bytes)?.try_into() 32 | } 33 | 34 | #[must_use] 35 | fn as_id(&self) -> &tg::Id { 36 | &self.0 37 | } 38 | } 39 | 40 | impl From for crate::Id { 41 | fn from(value: Id) -> Self { 42 | value.0 43 | } 44 | } 45 | 46 | impl TryFrom for Id { 47 | type Error = tg::Error; 48 | 49 | fn try_from(value: crate::Id) -> tg::Result { 50 | if value.kind() != tg::id::Kind::Process { 51 | return Err(tg::error!(%value, "invalid kind")); 52 | } 53 | Ok(Self(value)) 54 | } 55 | } 56 | 57 | impl std::str::FromStr for Id { 58 | type Err = tg::Error; 59 | 60 | fn from_str(s: &str) -> tg::Result { 61 | crate::Id::from_str(s)?.try_into() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/client/src/process/log.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use std::{fmt, str::FromStr}; 3 | 4 | pub mod get; 5 | pub mod post; 6 | 7 | #[derive(Clone, Copy, Debug, serde_with::DeserializeFromStr, serde_with::SerializeDisplay)] 8 | pub enum Stream { 9 | Stderr, 10 | Stdout, 11 | } 12 | 13 | impl fmt::Display for Stream { 14 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 15 | match self { 16 | Self::Stderr => write!(f, "stderr"), 17 | Self::Stdout => write!(f, "stdout"), 18 | } 19 | } 20 | } 21 | 22 | impl FromStr for Stream { 23 | type Err = tg::Error; 24 | fn from_str(s: &str) -> Result { 25 | match s { 26 | "stderr" => Ok(Self::Stderr), 27 | "stdout" => Ok(Self::Stdout), 28 | stream => Err(tg::error!(%stream, "unknown stream")), 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/client/src/process/log/post.rs: -------------------------------------------------------------------------------- 1 | use crate::{self as tg, util::serde::BytesBase64}; 2 | use bytes::Bytes; 3 | use serde_with::serde_as; 4 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 5 | 6 | #[serde_as] 7 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 8 | pub struct Arg { 9 | #[serde_as(as = "BytesBase64")] 10 | pub bytes: Bytes, 11 | 12 | pub stream: tg::process::log::Stream, 13 | 14 | #[serde(default, skip_serializing_if = "Option::is_none")] 15 | pub remote: Option, 16 | } 17 | 18 | impl tg::Process { 19 | pub async fn post_log(&self, handle: &H, arg: tg::process::log::post::Arg) -> tg::Result<()> 20 | where 21 | H: tg::Handle, 22 | { 23 | let id = self.id(); 24 | handle.post_process_log(id, arg).await?; 25 | Ok(()) 26 | } 27 | } 28 | 29 | impl tg::Client { 30 | pub async fn post_process_log( 31 | &self, 32 | id: &tg::process::Id, 33 | arg: tg::process::log::post::Arg, 34 | ) -> tg::Result<()> { 35 | let method = http::Method::POST; 36 | let uri = format!("/processes/{id}/log"); 37 | let request = http::request::Builder::default() 38 | .method(method) 39 | .uri(uri) 40 | .header( 41 | http::header::CONTENT_TYPE, 42 | mime::APPLICATION_JSON.to_string(), 43 | ) 44 | .json(arg) 45 | .unwrap(); 46 | let response = self.send(request).await?; 47 | if !response.status().is_success() { 48 | let error = response.json().await?; 49 | return Err(error); 50 | } 51 | Ok(()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/client/src/process/pty.rs: -------------------------------------------------------------------------------- 1 | pub struct Pty { 2 | rows: u64, 3 | columns: u64, 4 | } 5 | -------------------------------------------------------------------------------- /packages/client/src/process/put.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 5 | pub struct Arg { 6 | #[serde(flatten)] 7 | pub data: tg::process::Data, 8 | } 9 | 10 | impl Arg { 11 | pub fn objects(&self) -> Vec { 12 | self.data.objects() 13 | } 14 | } 15 | 16 | impl tg::Client { 17 | pub async fn put_process( 18 | &self, 19 | id: &tg::process::Id, 20 | arg: tg::process::put::Arg, 21 | ) -> tg::Result<()> { 22 | let method = http::Method::PUT; 23 | let uri = format!("/processes/{id}"); 24 | let request = http::request::Builder::default() 25 | .method(method) 26 | .uri(uri) 27 | .json(arg) 28 | .unwrap(); 29 | let response = self.send(request).await?; 30 | if !response.status().is_success() { 31 | let error = response.json().await?; 32 | return Err(error); 33 | } 34 | Ok(()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/client/src/process/signal/post.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] 5 | pub struct Arg { 6 | #[serde(default, skip_serializing_if = "Option::is_none")] 7 | pub remote: Option, 8 | 9 | pub signal: tg::process::Signal, 10 | } 11 | 12 | impl tg::Client { 13 | pub async fn post_process_signal(&self, id: &tg::process::Id, arg: Arg) -> tg::Result<()> { 14 | let method = http::Method::POST; 15 | let uri = format!("/processes/{id}/signal"); 16 | let request = http::request::Builder::default() 17 | .method(method) 18 | .uri(uri) 19 | .json(arg) 20 | .unwrap(); 21 | let response = self.send(request).await?; 22 | if !response.status().is_success() { 23 | return Err(response.json().await?); 24 | } 25 | Ok(()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/client/src/process/start.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] 5 | pub struct Arg { 6 | #[serde(default, skip_serializing_if = "Option::is_none")] 7 | pub remote: Option, 8 | } 9 | 10 | impl tg::Client { 11 | pub async fn start_process( 12 | &self, 13 | id: &tg::process::Id, 14 | arg: tg::process::start::Arg, 15 | ) -> tg::Result<()> { 16 | let method = http::Method::POST; 17 | let uri = format!("/processes/{id}/start"); 18 | let request = http::request::Builder::default() 19 | .method(method) 20 | .uri(uri) 21 | .json(arg) 22 | .unwrap(); 23 | let response = self.send(request).await?; 24 | if !response.status().is_success() { 25 | let error = response.json().await?; 26 | return Err(error); 27 | } 28 | Ok(()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/client/src/process/stdio.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | 3 | #[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] 4 | #[serde(untagged)] 5 | pub enum Stdio { 6 | Pipe(tg::pipe::Id), 7 | Pty(tg::pty::Id), 8 | } 9 | 10 | impl std::fmt::Display for Stdio { 11 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 12 | match self { 13 | Self::Pipe(id) => write!(f, "{id}"), 14 | Self::Pty(id) => write!(f, "{id}"), 15 | } 16 | } 17 | } 18 | 19 | impl std::str::FromStr for Stdio { 20 | type Err = tg::Error; 21 | fn from_str(s: &str) -> Result { 22 | let id = tg::Id::from_str(s)?; 23 | match id.kind() { 24 | tg::id::Kind::Pipe => Ok(Stdio::Pipe(id.try_into().unwrap())), 25 | tg::id::Kind::Pty => Ok(Stdio::Pty(id.try_into().unwrap())), 26 | _ => Err(tg::error!("invalid kind")), 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/client/src/process/touch.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] 5 | pub struct Arg { 6 | #[serde(default, skip_serializing_if = "Option::is_none")] 7 | pub remote: Option, 8 | } 9 | 10 | impl tg::Client { 11 | pub async fn touch_process( 12 | &self, 13 | id: &tg::process::Id, 14 | arg: tg::process::touch::Arg, 15 | ) -> tg::Result<()> { 16 | let method = http::Method::POST; 17 | let uri = format!("/processes/{id}/touch"); 18 | let request = http::request::Builder::default() 19 | .method(method) 20 | .uri(uri) 21 | .json(arg) 22 | .unwrap(); 23 | let response = self.send(request).await?; 24 | if !response.status().is_success() { 25 | let error = response.json().await?; 26 | return Err(error); 27 | } 28 | Ok(()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/client/src/pty.rs: -------------------------------------------------------------------------------- 1 | use crate::util::serde::BytesBase64; 2 | use bytes::Bytes; 3 | use serde_with::serde_as; 4 | 5 | pub use self::{id::Id, size::Size}; 6 | 7 | pub mod close; 8 | pub mod create; 9 | pub mod id; 10 | pub mod read; 11 | pub mod size; 12 | pub mod write; 13 | 14 | #[serde_as] 15 | #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] 16 | #[serde(rename_all = "snake_case", tag = "kind", content = "value")] 17 | pub enum Event { 18 | Chunk(#[serde_as(as = "BytesBase64")] Bytes), 19 | Size(Size), 20 | End, 21 | } 22 | -------------------------------------------------------------------------------- /packages/client/src/pty/close.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Default, Clone, Debug, serde::Deserialize, serde::Serialize)] 5 | pub struct Arg { 6 | #[serde(default, skip_serializing_if = "Option::is_none")] 7 | pub remote: Option, 8 | pub master: bool, 9 | } 10 | 11 | impl tg::Client { 12 | pub async fn close_pty(&self, id: &tg::pty::Id, arg: Arg) -> tg::Result<()> { 13 | let method = http::Method::POST; 14 | let query = serde_urlencoded::to_string(&arg).unwrap(); 15 | let uri = format!("/ptys/{id}/close?{query}"); 16 | let request = http::request::Builder::default() 17 | .method(method) 18 | .uri(uri) 19 | .empty() 20 | .unwrap(); 21 | let response = self.send(request).await?; 22 | if !response.status().is_success() { 23 | let error = response.json().await?; 24 | return Err(error); 25 | } 26 | Ok(()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/client/src/pty/create.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 5 | pub struct Arg { 6 | #[serde(default, skip_serializing_if = "Option::is_none")] 7 | pub remote: Option, 8 | 9 | pub size: tg::pty::Size, 10 | } 11 | 12 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 13 | pub struct Output { 14 | pub id: tg::pty::Id, 15 | } 16 | 17 | impl tg::Client { 18 | pub async fn create_pty(&self, arg: Arg) -> tg::Result { 19 | let method = http::Method::POST; 20 | let uri = "/ptys"; 21 | let request = http::request::Builder::default() 22 | .method(method) 23 | .uri(uri) 24 | .json(arg) 25 | .unwrap(); 26 | let response = self.send(request).await?; 27 | if !response.status().is_success() { 28 | let error = response.json().await?; 29 | return Err(error); 30 | } 31 | let output = response.json().await?; 32 | Ok(output) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/client/src/pty/id.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | 3 | #[derive( 4 | Clone, 5 | Debug, 6 | Eq, 7 | Hash, 8 | Ord, 9 | PartialEq, 10 | PartialOrd, 11 | derive_more::Display, 12 | serde::Deserialize, 13 | serde::Serialize, 14 | )] 15 | #[serde(into = "crate::Id", try_from = "crate::Id")] 16 | pub struct Id(pub(crate) crate::Id); 17 | 18 | impl Id { 19 | #[allow(clippy::new_without_default)] 20 | #[must_use] 21 | pub fn new() -> Self { 22 | Self(crate::Id::new_uuidv7(tg::id::Kind::Pty)) 23 | } 24 | } 25 | 26 | impl From for crate::Id { 27 | fn from(value: Id) -> Self { 28 | value.0 29 | } 30 | } 31 | 32 | impl TryFrom for Id { 33 | type Error = tg::Error; 34 | 35 | fn try_from(value: crate::Id) -> tg::Result { 36 | if value.kind() != tg::id::Kind::Pty { 37 | return Err(tg::error!(%value, "invalid kind")); 38 | } 39 | Ok(Self(value)) 40 | } 41 | } 42 | 43 | impl std::str::FromStr for Id { 44 | type Err = tg::Error; 45 | 46 | fn from_str(s: &str) -> tg::Result { 47 | crate::Id::from_str(s)?.try_into() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/client/src/pty/size.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Copy, Clone, Debug, serde::Deserialize, serde::Serialize)] 5 | pub struct Size { 6 | pub rows: u16, 7 | pub cols: u16, 8 | } 9 | 10 | impl tg::Client { 11 | pub async fn get_pty_size( 12 | &self, 13 | id: &tg::pty::Id, 14 | arg: tg::pty::read::Arg, 15 | ) -> tg::Result> { 16 | let method = http::Method::GET; 17 | let uri = format!("/ptys/{id}/size"); 18 | let request = http::request::Builder::default() 19 | .method(method) 20 | .uri(uri) 21 | .json(arg) 22 | .unwrap(); 23 | self.send(request) 24 | .await 25 | .map_err(|source| tg::error!(!source, "failed to get the response"))? 26 | .json() 27 | .await 28 | .map_err(|source| tg::error!(!source, "failed to deserialize the body")) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/client/src/pty/write.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use futures::{Stream, StreamExt as _}; 3 | use std::pin::Pin; 4 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 5 | 6 | #[derive(Default, Clone, Debug, serde::Deserialize, serde::Serialize)] 7 | pub struct Arg { 8 | #[serde(default, skip_serializing_if = "Option::is_none")] 9 | pub remote: Option, 10 | pub master: bool, 11 | } 12 | 13 | impl tg::Client { 14 | pub async fn write_pty( 15 | &self, 16 | id: &tg::pty::Id, 17 | arg: Arg, 18 | stream: Pin> + Send + 'static>>, 19 | ) -> tg::Result<()> { 20 | let method = http::Method::POST; 21 | let query = serde_urlencoded::to_string(arg).unwrap(); 22 | let uri = format!("/ptys/{id}/write?{query}"); 23 | 24 | // Create the body. 25 | let stream = stream.map(|e| match e { 26 | Ok(e) => e.try_into(), 27 | Err(e) => e.try_into(), 28 | }); 29 | 30 | // Create the request. 31 | let request = http::request::Builder::default() 32 | .method(method) 33 | .uri(uri) 34 | .sse(stream) 35 | .unwrap(); 36 | 37 | // Send the request. 38 | let response = self.send(request).await?; 39 | if !response.status().is_success() { 40 | let error = response.json().await?; 41 | return Err(error); 42 | } 43 | 44 | Ok(()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/client/src/referent.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use std::path::PathBuf; 3 | 4 | #[derive( 5 | Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize, 6 | )] 7 | pub struct Referent { 8 | pub item: T, 9 | 10 | #[serde(default, skip_serializing_if = "Option::is_none")] 11 | pub path: Option, 12 | 13 | #[serde(default, skip_serializing_if = "Option::is_none")] 14 | pub subpath: Option, 15 | 16 | #[serde(default, skip_serializing_if = "Option::is_none")] 17 | pub tag: Option, 18 | } 19 | 20 | impl Referent { 21 | pub fn with_item(item: T) -> Self { 22 | Self { 23 | item, 24 | path: None, 25 | subpath: None, 26 | tag: None, 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/client/src/remote.rs: -------------------------------------------------------------------------------- 1 | pub mod delete; 2 | pub mod get; 3 | pub mod list; 4 | pub mod put; 5 | -------------------------------------------------------------------------------- /packages/client/src/remote/delete.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | impl tg::Client { 5 | pub async fn delete_remote(&self, name: &str) -> tg::Result<()> { 6 | let method = http::Method::DELETE; 7 | let name = urlencoding::encode(name); 8 | let uri = format!("/remotes/{name}"); 9 | let request = http::request::Builder::default() 10 | .method(method) 11 | .uri(uri) 12 | .empty() 13 | .unwrap(); 14 | let response = self.send(request).await?; 15 | if !response.status().is_success() { 16 | let error = response.json().await?; 17 | return Err(error); 18 | } 19 | Ok(()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/client/src/remote/get.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | use url::Url; 4 | 5 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 6 | pub struct Output { 7 | pub name: String, 8 | pub url: Url, 9 | } 10 | 11 | impl tg::Client { 12 | pub async fn try_get_remote(&self, name: &str) -> tg::Result> { 13 | let method = http::Method::GET; 14 | let name = urlencoding::encode(name); 15 | let uri = format!("/remotes/{name}"); 16 | let request = http::request::Builder::default() 17 | .method(method) 18 | .uri(uri) 19 | .empty() 20 | .unwrap(); 21 | let response = self.send(request).await?; 22 | if response.status() == http::StatusCode::NOT_FOUND { 23 | return Ok(None); 24 | } 25 | if !response.status().is_success() { 26 | let error = response.json().await?; 27 | return Err(error); 28 | } 29 | let output = response.json().await?; 30 | Ok(Some(output)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/client/src/remote/list.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] 5 | pub struct Arg {} 6 | 7 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 8 | #[serde(transparent)] 9 | pub struct Output { 10 | pub data: Vec, 11 | } 12 | 13 | impl tg::Client { 14 | pub async fn list_remotes( 15 | &self, 16 | arg: tg::remote::list::Arg, 17 | ) -> tg::Result { 18 | let method = http::Method::GET; 19 | let query = serde_urlencoded::to_string(&arg).unwrap(); 20 | let uri = format!("/remotes?{query}"); 21 | let request = http::request::Builder::default().method(method).uri(uri); 22 | let request = request.empty().unwrap(); 23 | let response = self.send(request).await?; 24 | if !response.status().is_success() { 25 | let error = response.json().await?; 26 | return Err(error); 27 | } 28 | let output = response.json().await?; 29 | Ok(output) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/client/src/remote/put.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | use url::Url; 4 | 5 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 6 | pub struct Arg { 7 | pub url: Url, 8 | } 9 | 10 | impl tg::Client { 11 | pub async fn put_remote(&self, name: &str, arg: tg::remote::put::Arg) -> tg::Result<()> { 12 | let method = http::Method::PUT; 13 | let uri = format!("/remotes/{name}"); 14 | let request = http::request::Builder::default() 15 | .method(method) 16 | .uri(uri) 17 | .json(arg) 18 | .unwrap(); 19 | let response = self.send(request).await?; 20 | if !response.status().is_success() { 21 | let error = response.json().await?; 22 | return Err(error); 23 | } 24 | Ok(()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/client/src/symlink.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | data::Symlink as Data, handle::Symlink as Handle, id::Id, object::Symlink as Object, 3 | }; 4 | 5 | pub mod data; 6 | pub mod handle; 7 | pub mod id; 8 | pub mod object; 9 | -------------------------------------------------------------------------------- /packages/client/src/symlink/data.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use bytes::Bytes; 3 | use itertools::Itertools as _; 4 | use std::{collections::BTreeSet, path::PathBuf}; 5 | 6 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 7 | #[serde(untagged)] 8 | pub enum Symlink { 9 | Graph { 10 | graph: tg::graph::Id, 11 | node: usize, 12 | }, 13 | 14 | Target { 15 | target: PathBuf, 16 | }, 17 | 18 | Artifact { 19 | artifact: tg::artifact::Id, 20 | 21 | #[serde(default, skip_serializing_if = "Option::is_none")] 22 | subpath: Option, 23 | }, 24 | } 25 | 26 | impl Symlink { 27 | pub fn serialize(&self) -> tg::Result { 28 | serde_json::to_vec(self) 29 | .map(Into::into) 30 | .map_err(|source| tg::error!(!source, "failed to serialize the data")) 31 | } 32 | 33 | pub fn deserialize<'a>(bytes: impl Into>) -> tg::Result { 34 | serde_json::from_reader(bytes.into().as_ref()) 35 | .map_err(|source| tg::error!(!source, "failed to deserialize the data")) 36 | } 37 | 38 | #[must_use] 39 | pub fn children(&self) -> BTreeSet { 40 | match self { 41 | Self::Graph { graph, .. } => std::iter::once(graph.clone()).map_into().collect(), 42 | Self::Target { .. } => BTreeSet::new(), 43 | Self::Artifact { artifact, .. } => { 44 | std::iter::once(artifact.clone()).map_into().collect() 45 | }, 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/client/src/symlink/id.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | 3 | #[derive( 4 | Clone, 5 | Debug, 6 | Eq, 7 | Hash, 8 | Ord, 9 | PartialEq, 10 | PartialOrd, 11 | derive_more::Display, 12 | derive_more::Into, 13 | serde::Deserialize, 14 | serde::Serialize, 15 | )] 16 | #[serde(into = "crate::Id", try_from = "crate::Id")] 17 | pub struct Id(pub(crate) crate::Id); 18 | 19 | impl Id { 20 | #[must_use] 21 | pub fn new(bytes: &[u8]) -> Self { 22 | Self(crate::Id::new_blake3(tg::id::Kind::Symlink, bytes)) 23 | } 24 | } 25 | 26 | impl TryFrom for Id { 27 | type Error = tg::Error; 28 | 29 | fn try_from(value: crate::Id) -> tg::Result { 30 | if value.kind() != tg::id::Kind::Symlink { 31 | return Err(tg::error!(%value, "invalid kind")); 32 | } 33 | Ok(Self(value)) 34 | } 35 | } 36 | 37 | impl std::str::FromStr for Id { 38 | type Err = tg::Error; 39 | 40 | fn from_str(s: &str) -> tg::Result { 41 | crate::Id::from_str(s)?.try_into() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/client/src/symlink/object.rs: -------------------------------------------------------------------------------- 1 | use super::Data; 2 | use crate as tg; 3 | use itertools::Itertools as _; 4 | use std::path::PathBuf; 5 | 6 | #[derive(Clone, Debug)] 7 | pub enum Symlink { 8 | Graph { 9 | graph: tg::Graph, 10 | node: usize, 11 | }, 12 | Target { 13 | target: PathBuf, 14 | }, 15 | Artifact { 16 | artifact: tg::Artifact, 17 | subpath: Option, 18 | }, 19 | } 20 | 21 | impl Symlink { 22 | #[must_use] 23 | pub fn children(&self) -> Vec { 24 | match self { 25 | Self::Graph { graph, .. } => std::iter::once(graph.clone()).map_into().collect(), 26 | Self::Target { .. } => vec![], 27 | Self::Artifact { artifact, .. } => { 28 | std::iter::once(artifact.clone()).map_into().collect() 29 | }, 30 | } 31 | } 32 | } 33 | 34 | impl TryFrom for Symlink { 35 | type Error = tg::Error; 36 | 37 | fn try_from(data: Data) -> Result { 38 | match data { 39 | Data::Graph { graph, node } => { 40 | let graph = tg::Graph::with_id(graph); 41 | Ok(Self::Graph { graph, node }) 42 | }, 43 | Data::Target { target } => Ok(Self::Target { target }), 44 | Data::Artifact { artifact, subpath } => { 45 | let artifact = tg::Artifact::with_id(artifact); 46 | Ok(Self::Artifact { artifact, subpath }) 47 | }, 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/client/src/tag/delete.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | impl tg::Client { 5 | pub async fn delete_tag(&self, tag: &tg::Tag) -> tg::Result<()> { 6 | let method = http::Method::DELETE; 7 | let uri = format!("/tags/{tag}"); 8 | let request = http::request::Builder::default() 9 | .method(method) 10 | .uri(uri) 11 | .empty() 12 | .unwrap(); 13 | let response = self.send(request).await?; 14 | if !response.status().is_success() { 15 | let error = response.json().await?; 16 | return Err(error); 17 | } 18 | Ok(()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/client/src/tag/get.rs: -------------------------------------------------------------------------------- 1 | use crate as tg; 2 | use tangram_either::Either; 3 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 4 | 5 | #[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Hash, serde::Deserialize, serde::Serialize)] 6 | pub struct Output { 7 | pub tag: tg::Tag, 8 | 9 | pub item: Either, 10 | } 11 | 12 | impl tg::Client { 13 | pub async fn try_get_tag( 14 | &self, 15 | pattern: &tg::tag::Pattern, 16 | ) -> tg::Result> { 17 | let method = http::Method::GET; 18 | let uri = format!("/tags/{pattern}"); 19 | let request = http::request::Builder::default() 20 | .method(method) 21 | .uri(uri) 22 | .empty() 23 | .unwrap(); 24 | let response = self.send(request).await?; 25 | if response.status() == http::StatusCode::NOT_FOUND { 26 | return Ok(None); 27 | } 28 | if !response.status().is_success() { 29 | let error = response.json().await?; 30 | return Err(error); 31 | } 32 | let output = response.json().await?; 33 | Ok(Some(output)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/client/src/tag/list.rs: -------------------------------------------------------------------------------- 1 | use crate::{self as tg, util::serde::is_false}; 2 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 3 | 4 | #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] 5 | pub struct Arg { 6 | #[serde(default, skip_serializing_if = "Option::is_none")] 7 | pub length: Option, 8 | 9 | #[serde(default, skip_serializing_if = "tg::tag::Pattern::is_empty")] 10 | pub pattern: tg::tag::Pattern, 11 | 12 | #[serde(default, skip_serializing_if = "Option::is_none")] 13 | pub remote: Option, 14 | 15 | #[serde(default, skip_serializing_if = "is_false")] 16 | pub reverse: bool, 17 | } 18 | 19 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 20 | #[serde(transparent)] 21 | pub struct Output { 22 | pub data: Vec, 23 | } 24 | 25 | impl tg::Client { 26 | pub async fn list_tags(&self, arg: tg::tag::list::Arg) -> tg::Result { 27 | let method = http::Method::GET; 28 | let query = serde_urlencoded::to_string(&arg).unwrap(); 29 | let uri = format!("/tags?{query}"); 30 | let request = http::request::Builder::default().method(method).uri(uri); 31 | let request = request.empty().unwrap(); 32 | let response = self.send(request).await?; 33 | if !response.status().is_success() { 34 | let error = response.json().await?; 35 | return Err(error); 36 | } 37 | let output = response.json().await?; 38 | Ok(output) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/client/src/tag/put.rs: -------------------------------------------------------------------------------- 1 | use crate::{self as tg, util::serde::is_false}; 2 | use tangram_either::Either; 3 | use tangram_http::{request::builder::Ext as _, response::Ext as _}; 4 | 5 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 6 | pub struct Arg { 7 | #[serde(default, skip_serializing_if = "is_false")] 8 | pub force: bool, 9 | 10 | pub item: Either, 11 | 12 | #[serde(default, skip_serializing_if = "Option::is_none")] 13 | pub remote: Option, 14 | } 15 | 16 | impl tg::Client { 17 | pub async fn put_tag(&self, tag: &tg::Tag, arg: tg::tag::put::Arg) -> tg::Result<()> { 18 | let method = http::Method::PUT; 19 | let uri = format!("/tags/{tag}"); 20 | let request = http::request::Builder::default() 21 | .method(method) 22 | .uri(uri) 23 | .json(arg) 24 | .unwrap(); 25 | let response = self.send(request).await?; 26 | if !response.status().is_success() { 27 | let error = response.json().await?; 28 | return Err(error); 29 | } 30 | Ok(()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/client/src/util.rs: -------------------------------------------------------------------------------- 1 | pub mod arc; 2 | pub mod serde; 3 | -------------------------------------------------------------------------------- /packages/client/src/util/arc.rs: -------------------------------------------------------------------------------- 1 | use std::{ops::Deref, sync::Arc}; 2 | 3 | pub trait Ext: Sized { 4 | fn map(self, f: F) -> Map 5 | where 6 | F: Fn(&Self) -> &U, 7 | { 8 | Map::new(self, f) 9 | } 10 | } 11 | 12 | impl Ext for Arc { 13 | fn map(self, f: F) -> Map 14 | where 15 | F: Fn(&Self) -> &U, 16 | { 17 | Map::new(self, f) 18 | } 19 | } 20 | 21 | pub struct Map 22 | where 23 | F: Fn(&T) -> &U, 24 | { 25 | value: T, 26 | f: F, 27 | } 28 | 29 | impl Map 30 | where 31 | F: Fn(&T) -> &U, 32 | { 33 | pub fn new(value: T, f: F) -> Self { 34 | Self { value, f } 35 | } 36 | } 37 | 38 | impl Deref for Map 39 | where 40 | F: Fn(&T) -> &U, 41 | { 42 | type Target = U; 43 | 44 | fn deref(&self) -> &Self::Target { 45 | (self.f)(&self.value) 46 | } 47 | } 48 | 49 | impl AsRef for Map 50 | where 51 | F: Fn(&T) -> &U, 52 | { 53 | fn as_ref(&self) -> &U { 54 | (self.f)(&self.value) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/compiler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tangramdotdev/compiler", 3 | "private": true, 4 | "scripts": { 5 | "check": "tsc && biome check src", 6 | "format": "biome check --write src" 7 | }, 8 | "type": "module" 9 | } 10 | -------------------------------------------------------------------------------- /packages/compiler/src/assert.ts: -------------------------------------------------------------------------------- 1 | export let assert: (condition: unknown, message?: string) => asserts condition = 2 | (condition, message) => { 3 | if (!condition) { 4 | throw new Error(message ?? "failed assertion"); 5 | } 6 | }; 7 | 8 | export let unimplemented = (message?: string): never => { 9 | throw new Error(message ?? "reached unimplemented code"); 10 | }; 11 | 12 | export let unreachable = (message?: string): never => { 13 | throw new Error(message ?? "reached unreachable code"); 14 | }; 15 | 16 | export let todo = (): never => { 17 | throw new Error("reached todo"); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/compiler/src/check.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import type { Diagnostic } from "./diagnostics.ts"; 3 | import type { Module } from "./module.ts"; 4 | import * as typescript from "./typescript.ts"; 5 | 6 | export type Request = { 7 | modules: Array; 8 | }; 9 | 10 | export type Response = { 11 | diagnostics: Array; 12 | }; 13 | 14 | export let handle = (request: Request): Response => { 15 | let diagnostics = []; 16 | 17 | // Create a typescript program. 18 | let program = ts.createProgram({ 19 | rootNames: request.modules.map(typescript.fileNameFromModule), 20 | options: typescript.compilerOptions, 21 | host: typescript.host, 22 | }); 23 | 24 | // Collect the TypeScript diagnostics. 25 | diagnostics.push( 26 | ...[ 27 | ...program.getConfigFileParsingDiagnostics(), 28 | ...program.getOptionsDiagnostics(), 29 | ...program.getGlobalDiagnostics(), 30 | ...program.getDeclarationDiagnostics(), 31 | ...program.getSyntacticDiagnostics(), 32 | ...program.getSemanticDiagnostics(), 33 | ].map(typescript.convertDiagnostic), 34 | ); 35 | 36 | return { diagnostics }; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/compiler/src/completion.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import type { Module } from "./module.ts"; 3 | import type { Position } from "./position.ts"; 4 | import * as typescript from "./typescript.ts"; 5 | 6 | export type Request = { 7 | module: Module; 8 | position: Position; 9 | }; 10 | 11 | export type Response = { 12 | entries: Array | undefined; 13 | }; 14 | 15 | export type CompletionEntry = { 16 | name: string; 17 | }; 18 | 19 | export let handle = (request: Request): Response => { 20 | // Get the source file and position. 21 | let sourceFile = typescript.host.getSourceFile( 22 | typescript.fileNameFromModule(request.module), 23 | ts.ScriptTarget.ESNext, 24 | ); 25 | if (sourceFile === undefined) { 26 | throw new Error(); 27 | } 28 | let position = ts.getPositionOfLineAndCharacter( 29 | sourceFile, 30 | request.position.line, 31 | request.position.character, 32 | ); 33 | 34 | // Get the completions. 35 | let info = typescript.languageService.getCompletionsAtPosition( 36 | typescript.fileNameFromModule(request.module), 37 | position, 38 | undefined, 39 | ); 40 | 41 | // Convert the completion entries. 42 | let entries = info?.entries.map((entry) => ({ name: entry.name })); 43 | 44 | return { 45 | entries, 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /packages/compiler/src/diagnostics.ts: -------------------------------------------------------------------------------- 1 | import type { Location } from "./location.ts"; 2 | import * as typescript from "./typescript.ts"; 3 | 4 | export type Request = unknown; 5 | 6 | export type Response = { 7 | diagnostics: Array; 8 | }; 9 | 10 | export type Diagnostic = { 11 | location: Location | null; 12 | severity: Severity; 13 | message: string; 14 | }; 15 | 16 | export type Severity = "error" | "warning" | "info" | "hint"; 17 | 18 | export let handle = (_request: Request): Response => { 19 | // Get the documents. 20 | let documents = syscall("document_list"); 21 | 22 | // Collect the diagnostics. 23 | let diagnostics = documents 24 | .flatMap((module) => { 25 | let fileName = typescript.fileNameFromModule(module); 26 | return [ 27 | ...typescript.languageService.getSyntacticDiagnostics(fileName), 28 | ...typescript.languageService.getSemanticDiagnostics(fileName), 29 | ...typescript.languageService.getSuggestionDiagnostics(fileName), 30 | ]; 31 | }) 32 | .map(typescript.convertDiagnostic); 33 | 34 | return { 35 | diagnostics, 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/compiler/src/error.ts: -------------------------------------------------------------------------------- 1 | import type { Module } from "./module.ts"; 2 | 3 | export class Error_ { 4 | message: string; 5 | location: Location | undefined; 6 | stack: Array | undefined; 7 | source: Error_ | undefined; 8 | values: Map | undefined; 9 | 10 | constructor( 11 | message: string, 12 | location?: Location, 13 | stack?: Array, 14 | source?: Error_, 15 | values?: Map, 16 | ) { 17 | this.message = message; 18 | this.location = location; 19 | this.stack = stack; 20 | this.source = source; 21 | this.values = values; 22 | } 23 | } 24 | 25 | type Location = { 26 | symbol?: string; 27 | source: Source; 28 | line: number; 29 | column: number; 30 | }; 31 | 32 | type Source = 33 | | { kind: "internal"; value: string } 34 | | { kind: "external"; value: Module }; 35 | -------------------------------------------------------------------------------- /packages/compiler/src/hover.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import type { Module } from "./module.ts"; 3 | import type { Position } from "./position.ts"; 4 | import * as typescript from "./typescript.ts"; 5 | 6 | export type Request = { 7 | module: Module; 8 | position: Position; 9 | }; 10 | 11 | export type Response = { 12 | text: string | undefined; 13 | }; 14 | 15 | export let handle = (request: Request): Response => { 16 | // Get the source file. 17 | let sourceFile = typescript.host.getSourceFile( 18 | typescript.fileNameFromModule(request.module), 19 | ts.ScriptTarget.ESNext, 20 | ); 21 | if (sourceFile === undefined) { 22 | throw new Error(); 23 | } 24 | 25 | // Get the position of the hover. 26 | let position = ts.getPositionOfLineAndCharacter( 27 | sourceFile, 28 | request.position.line, 29 | request.position.character, 30 | ); 31 | 32 | // Get the quick info at the position. 33 | let quickInfo = typescript.languageService.getQuickInfoAtPosition( 34 | typescript.fileNameFromModule(request.module), 35 | position, 36 | ); 37 | 38 | // Get the text. 39 | let text = quickInfo?.displayParts?.map(({ text }) => text).join(""); 40 | 41 | return { text }; 42 | }; 43 | -------------------------------------------------------------------------------- /packages/compiler/src/location.ts: -------------------------------------------------------------------------------- 1 | import type { Module } from "./module.ts"; 2 | import type { Range } from "./range.ts"; 3 | 4 | export type Location = { 5 | module: Module; 6 | range: Range; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/compiler/src/module.ts: -------------------------------------------------------------------------------- 1 | export type Module = { 2 | kind: Module.Kind; 3 | referent: Referent; 4 | }; 5 | 6 | type Referent = { 7 | item: string; 8 | path?: string | undefined; 9 | subpath?: string | undefined; 10 | tag?: string | undefined; 11 | }; 12 | 13 | export namespace Module { 14 | export type Kind = 15 | | "js" 16 | | "ts" 17 | | "dts" 18 | | "object" 19 | | "artifact" 20 | | "blob" 21 | | "directory" 22 | | "file" 23 | | "symlink" 24 | | "graph" 25 | | "command"; 26 | } 27 | -------------------------------------------------------------------------------- /packages/compiler/src/position.ts: -------------------------------------------------------------------------------- 1 | export type Position = { 2 | line: number; 3 | character: number; 4 | }; 5 | -------------------------------------------------------------------------------- /packages/compiler/src/range.ts: -------------------------------------------------------------------------------- 1 | import type { Position } from "./position.ts"; 2 | 3 | export type Range = { 4 | start: Position; 5 | end: Position; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/compiler/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowImportingTsExtensions": true, 4 | "exactOptionalPropertyTypes": true, 5 | "isolatedModules": true, 6 | "lib": ["esnext"], 7 | "module": "esnext", 8 | "moduleResolution": "bundler", 9 | "noEmit": true, 10 | "noUncheckedIndexedAccess": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "strict": true, 14 | "target": "esnext", 15 | "types": [] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/database/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tangram_database" 3 | 4 | description = { workspace = true } 5 | edition = { workspace = true } 6 | homepage = { workspace = true } 7 | license = { workspace = true } 8 | publish = false 9 | repository = { workspace = true } 10 | rust-version = { workspace = true } 11 | version = { workspace = true } 12 | 13 | [lints] 14 | workspace = true 15 | 16 | [dependencies] 17 | bytes = { workspace = true } 18 | derive_more = { workspace = true } 19 | fnv = { workspace = true } 20 | futures = { workspace = true } 21 | indexmap = { workspace = true } 22 | itertools = { workspace = true } 23 | num = { workspace = true } 24 | rusqlite = { workspace = true } 25 | serde = { workspace = true } 26 | serde_json = { workspace = true } 27 | tangram_either = { workspace = true } 28 | tokio = { workspace = true } 29 | tokio-postgres = { workspace = true } 30 | tracing = { workspace = true } 31 | url = { workspace = true } 32 | -------------------------------------------------------------------------------- /packages/database/src/row.rs: -------------------------------------------------------------------------------- 1 | use crate::Value; 2 | use indexmap::IndexMap; 3 | use tangram_either::Either; 4 | 5 | #[derive(Clone, Debug)] 6 | pub struct Row { 7 | entries: IndexMap, 8 | } 9 | 10 | impl Row { 11 | pub fn with_entries(entries: impl IntoIterator) -> Self { 12 | let entries = entries.into_iter().collect(); 13 | Self { entries } 14 | } 15 | 16 | pub fn entries(&self) -> impl Iterator { 17 | self.entries.iter() 18 | } 19 | 20 | pub fn into_entries(self) -> impl Iterator { 21 | self.entries.into_iter() 22 | } 23 | 24 | pub fn into_values(self) -> impl Iterator { 25 | self.entries.into_values() 26 | } 27 | 28 | pub fn get<'a>(&self, index: impl Into>) -> Option<&Value> { 29 | match index.into() { 30 | Either::Left(index) => self.entries.get_index(index).map(|(_, value)| value), 31 | Either::Right(index) => self.entries.get(index), 32 | } 33 | } 34 | } 35 | 36 | impl<'de> serde::Deserializer<'de> for Row { 37 | type Error = crate::value::de::Error; 38 | 39 | fn deserialize_any(self, visitor: V) -> Result 40 | where 41 | V: serde::de::Visitor<'de>, 42 | { 43 | visitor.visit_map(serde::de::value::MapDeserializer::new( 44 | self.entries.into_iter(), 45 | )) 46 | } 47 | 48 | serde::forward_to_deserialize_any!(bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string bytes byte_buf option unit unit_struct newtype_struct seq tuple tuple_struct map struct enum identifier ignored_any); 49 | } 50 | -------------------------------------------------------------------------------- /packages/database/src/value.rs: -------------------------------------------------------------------------------- 1 | pub use self::json::Json; 2 | 3 | pub mod de; 4 | pub mod json; 5 | pub mod ser; 6 | 7 | #[derive( 8 | Clone, 9 | Debug, 10 | derive_more::From, 11 | derive_more::IsVariant, 12 | derive_more::TryInto, 13 | derive_more::TryUnwrap, 14 | serde::Deserialize, 15 | serde::Serialize, 16 | )] 17 | #[serde(untagged)] 18 | #[try_unwrap(ref)] 19 | pub enum Value { 20 | Null, 21 | Integer(i64), 22 | Real(f64), 23 | Text(String), 24 | Blob(Vec), 25 | } 26 | -------------------------------------------------------------------------------- /packages/database/src/value/json.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Default)] 2 | pub struct Json(pub T); 3 | 4 | impl serde::Serialize for Json 5 | where 6 | T: serde::Serialize, 7 | { 8 | fn serialize(&self, serializer: S) -> Result 9 | where 10 | S: serde::Serializer, 11 | { 12 | let json = serde_json::to_string(&self.0).map_err(serde::ser::Error::custom)?; 13 | serializer.serialize_str(&json) 14 | } 15 | } 16 | 17 | impl<'de, T> serde::Deserialize<'de> for Json 18 | where 19 | T: serde::de::DeserializeOwned, 20 | { 21 | fn deserialize(deserializer: D) -> Result 22 | where 23 | D: serde::Deserializer<'de>, 24 | { 25 | let json = String::deserialize(deserializer)?; 26 | let value = serde_json::from_str(&json).map_err(serde::de::Error::custom)?; 27 | Ok(Json(value)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/either/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tangram_either" 3 | 4 | description = { workspace = true } 5 | edition = { workspace = true } 6 | homepage = { workspace = true } 7 | license = { workspace = true } 8 | publish = false 9 | repository = { workspace = true } 10 | rust-version = { workspace = true } 11 | version = { workspace = true } 12 | 13 | [lints] 14 | workspace = true 15 | 16 | [dependencies] 17 | serde = { workspace = true } 18 | -------------------------------------------------------------------------------- /packages/futures/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tangram_futures" 3 | 4 | description = { workspace = true } 5 | edition = { workspace = true } 6 | homepage = { workspace = true } 7 | license = { workspace = true } 8 | publish = false 9 | repository = { workspace = true } 10 | rust-version = { workspace = true } 11 | version = { workspace = true } 12 | 13 | [lints] 14 | workspace = true 15 | 16 | [dependencies] 17 | dashmap = { workspace = true } 18 | futures = { workspace = true } 19 | itertools = { workspace = true } 20 | pin-project = { workspace = true } 21 | tokio = { workspace = true } 22 | -------------------------------------------------------------------------------- /packages/futures/src/attach.rs: -------------------------------------------------------------------------------- 1 | use futures::Stream; 2 | use std::{pin::Pin, task::Poll}; 3 | use tokio::io::AsyncRead; 4 | 5 | pub struct Attach { 6 | inner: I, 7 | #[allow(dead_code)] 8 | value: T, 9 | } 10 | 11 | impl Attach { 12 | pub fn new(inner: I, value: T) -> Self { 13 | Self { inner, value } 14 | } 15 | } 16 | 17 | impl Future for Attach 18 | where 19 | I: Future, 20 | { 21 | type Output = I::Output; 22 | 23 | fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { 24 | let future = unsafe { self.as_mut().map_unchecked_mut(|s| &mut s.inner) }; 25 | future.poll(cx) 26 | } 27 | } 28 | 29 | impl Stream for Attach 30 | where 31 | I: Stream, 32 | { 33 | type Item = I::Item; 34 | 35 | fn poll_next( 36 | mut self: Pin<&mut Self>, 37 | cx: &mut std::task::Context<'_>, 38 | ) -> Poll> { 39 | let stream = unsafe { self.as_mut().map_unchecked_mut(|s| &mut s.inner) }; 40 | stream.poll_next(cx) 41 | } 42 | } 43 | 44 | impl AsyncRead for Attach 45 | where 46 | I: AsyncRead, 47 | { 48 | fn poll_read( 49 | mut self: Pin<&mut Self>, 50 | cx: &mut std::task::Context<'_>, 51 | buf: &mut tokio::io::ReadBuf<'_>, 52 | ) -> Poll> { 53 | let reader = unsafe { self.as_mut().map_unchecked_mut(|s| &mut s.inner) }; 54 | reader.poll_read(cx, buf) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/futures/src/either.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | use tokio::io::AsyncRead; 3 | 4 | pub enum Either { 5 | Left(L), 6 | Right(R), 7 | } 8 | 9 | impl Either { 10 | #[must_use] 11 | pub fn as_pin_mut(self: Pin<&mut Self>) -> Either, Pin<&mut R>> { 12 | unsafe { 13 | match self.get_unchecked_mut() { 14 | Self::Left(left) => Either::Left(Pin::new_unchecked(left)), 15 | Self::Right(right) => Either::Right(Pin::new_unchecked(right)), 16 | } 17 | } 18 | } 19 | } 20 | 21 | impl AsyncRead for Either 22 | where 23 | L: AsyncRead, 24 | R: AsyncRead, 25 | { 26 | fn poll_read( 27 | self: Pin<&mut Self>, 28 | cx: &mut std::task::Context<'_>, 29 | buf: &mut tokio::io::ReadBuf<'_>, 30 | ) -> std::task::Poll> { 31 | match self.as_pin_mut() { 32 | Either::Left(s) => s.poll_read(cx, buf), 33 | Either::Right(s) => s.poll_read(cx, buf), 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/futures/src/future.rs: -------------------------------------------------------------------------------- 1 | use crate::attach::Attach; 2 | use futures::Future; 3 | 4 | pub trait Ext: Future { 5 | fn attach(self, value: T) -> Attach 6 | where 7 | Self: Sized, 8 | { 9 | Attach::new(self, value) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/futures/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod attach; 2 | pub mod either; 3 | pub mod future; 4 | pub mod read; 5 | pub mod stream; 6 | pub mod task; 7 | pub mod write; 8 | -------------------------------------------------------------------------------- /packages/futures/src/stream.rs: -------------------------------------------------------------------------------- 1 | use self::take_while_inclusive::TakeWhileInclusive; 2 | use crate::attach::Attach; 3 | use futures::{Stream, StreamExt as _, TryStream, TryStreamExt as _}; 4 | 5 | pub mod take_while_inclusive; 6 | 7 | pub trait Ext: Stream { 8 | fn attach(self, value: T) -> Attach 9 | where 10 | Self: Sized, 11 | { 12 | Attach::new(self, value) 13 | } 14 | 15 | fn last(mut self) -> impl Future> 16 | where 17 | Self: Sized + Unpin, 18 | { 19 | async move { 20 | let mut last = None; 21 | while let Some(item) = self.next().await { 22 | last = Some(item); 23 | } 24 | last 25 | } 26 | } 27 | 28 | fn take_while_inclusive(self, predicate: F) -> TakeWhileInclusive 29 | where 30 | Self: Sized, 31 | F: FnMut(&Self::Item) -> Fut, 32 | Fut: Future, 33 | { 34 | TakeWhileInclusive::new(self, predicate) 35 | } 36 | } 37 | 38 | impl Ext for S where S: Stream {} 39 | 40 | pub trait TryExt: TryStream { 41 | fn try_last(mut self) -> impl Future, Self::Error>> 42 | where 43 | Self: Sized + Unpin, 44 | { 45 | async move { 46 | let mut last = None; 47 | while let Some(item) = self.try_next().await? { 48 | last = Some(item); 49 | } 50 | Ok(last) 51 | } 52 | } 53 | } 54 | 55 | impl TryExt for S where S: TryStream {} 56 | -------------------------------------------------------------------------------- /packages/futures/src/write.rs: -------------------------------------------------------------------------------- 1 | use futures::Future; 2 | use std::pin::Pin; 3 | use tokio::io::{AsyncWrite, AsyncWriteExt as _}; 4 | 5 | pub type Boxed<'a> = Pin>; 6 | 7 | pub trait Ext: AsyncWrite { 8 | fn boxed<'a>(self) -> Boxed<'a> 9 | where 10 | Self: Sized + Send + 'a, 11 | { 12 | Box::pin(self) 13 | } 14 | 15 | fn write_uvarint( 16 | &mut self, 17 | mut value: u64, 18 | ) -> impl Future> + Send + '_ 19 | where 20 | Self: Unpin + Send, 21 | { 22 | async move { 23 | loop { 24 | let mut byte = (value & 0x7F) as u8; 25 | value >>= 7; 26 | if value != 0 { 27 | byte |= 0x80; 28 | } 29 | self.write_all(&[byte]).await?; 30 | if value == 0 { 31 | break; 32 | } 33 | } 34 | Ok(()) 35 | } 36 | } 37 | } 38 | 39 | impl Ext for T where T: AsyncWrite {} 40 | -------------------------------------------------------------------------------- /packages/http/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tangram_http" 3 | 4 | description = { workspace = true } 5 | edition = { workspace = true } 6 | homepage = { workspace = true } 7 | license = { workspace = true } 8 | publish = false 9 | repository = { workspace = true } 10 | rust-version = { workspace = true } 11 | version = { workspace = true } 12 | 13 | [lints] 14 | workspace = true 15 | 16 | [dependencies] 17 | async-compression = { workspace = true } 18 | bytes = { workspace = true } 19 | derive_more = { workspace = true } 20 | erased-serde = { workspace = true } 21 | futures = { workspace = true } 22 | http = { workspace = true } 23 | http-body = { workspace = true } 24 | http-body-util = { workspace = true } 25 | hyper = { workspace = true } 26 | pin-project = { workspace = true } 27 | serde = { workspace = true } 28 | serde_json = { workspace = true } 29 | serde_urlencoded = { workspace = true } 30 | sync_wrapper = { workspace = true } 31 | tracing = { workspace = true } 32 | tokio = { workspace = true } 33 | tokio-stream = { workspace = true } 34 | tokio-util = { workspace = true } 35 | tower = { workspace = true } 36 | tower-http = { workspace = true } -------------------------------------------------------------------------------- /packages/http/src/body/and_then_frame.rs: -------------------------------------------------------------------------------- 1 | use bytes::Buf; 2 | use http_body::{Body, Frame}; 3 | use pin_project::pin_project; 4 | use std::{ 5 | pin::Pin, 6 | task::{Context, Poll}, 7 | }; 8 | 9 | #[pin_project] 10 | #[derive(Clone, Copy)] 11 | pub struct AndThenFrame { 12 | #[pin] 13 | inner: B, 14 | f: F, 15 | } 16 | 17 | impl AndThenFrame { 18 | pub(crate) fn new(body: B, f: F) -> Self { 19 | Self { inner: body, f } 20 | } 21 | } 22 | 23 | impl Body for AndThenFrame 24 | where 25 | B: Body, 26 | F: FnMut(Frame) -> Result, B::Error>, 27 | B2: Buf, 28 | { 29 | type Data = B2; 30 | type Error = B::Error; 31 | 32 | fn poll_frame( 33 | self: Pin<&mut Self>, 34 | cx: &mut Context<'_>, 35 | ) -> Poll, Self::Error>>> { 36 | let this = self.project(); 37 | match this.inner.poll_frame(cx) { 38 | Poll::Pending => Poll::Pending, 39 | Poll::Ready(None) => Poll::Ready(None), 40 | Poll::Ready(Some(Ok(frame))) => Poll::Ready(Some((this.f)(frame))), 41 | Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))), 42 | } 43 | } 44 | 45 | fn is_end_stream(&self) -> bool { 46 | self.inner.is_end_stream() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/http/src/header.rs: -------------------------------------------------------------------------------- 1 | pub mod accept_encoding; 2 | pub mod content_encoding; 3 | -------------------------------------------------------------------------------- /packages/http/src/header/content_encoding.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 2 | pub enum ContentEncoding { 3 | Zstd, 4 | } 5 | 6 | #[derive(Debug, derive_more::Display, derive_more::Error)] 7 | pub struct FromStrError; 8 | 9 | impl std::fmt::Display for ContentEncoding { 10 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 11 | match self { 12 | ContentEncoding::Zstd => write!(f, "zstd"), 13 | } 14 | } 15 | } 16 | 17 | impl std::str::FromStr for ContentEncoding { 18 | type Err = FromStrError; 19 | 20 | fn from_str(s: &str) -> Result { 21 | match s { 22 | "zstd" => Ok(ContentEncoding::Zstd), 23 | _ => Err(FromStrError), 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/http/src/layer.rs: -------------------------------------------------------------------------------- 1 | pub mod compression; 2 | pub mod tracing; 3 | -------------------------------------------------------------------------------- /packages/http/src/layer/tracing.rs: -------------------------------------------------------------------------------- 1 | use crate::{Request, Response}; 2 | use tower::ServiceExt as _; 3 | 4 | #[derive(Clone)] 5 | pub struct TracingLayer { 6 | request: bool, 7 | response: bool, 8 | } 9 | 10 | impl TracingLayer { 11 | #[must_use] 12 | pub fn new() -> Self { 13 | Self { 14 | request: true, 15 | response: true, 16 | } 17 | } 18 | } 19 | 20 | impl Default for TracingLayer { 21 | fn default() -> Self { 22 | Self::new() 23 | } 24 | } 25 | 26 | impl tower::layer::Layer for TracingLayer 27 | where 28 | S: tower::Service + Clone + Send + Sync + 'static, 29 | S::Future: Send + 'static, 30 | S::Error: Send + 'static, 31 | { 32 | type Service = tower::util::BoxCloneSyncService; 33 | 34 | fn layer(&self, service: S) -> Self::Service { 35 | let layer = self.clone(); 36 | tower::util::BoxCloneSyncService::new(tower::service_fn(move |request: Request| { 37 | let layer = layer.clone(); 38 | let mut service = service.clone(); 39 | async move { 40 | if layer.request { 41 | tracing::trace!(headers = ?request.headers(), method = ?request.method(), path = ?request.uri().path(), "request"); 42 | } 43 | let response = service.ready().await?.call(request).await?; 44 | if layer.response { 45 | tracing::trace!(headers = ?response.headers(), status = ?response.status(), "response"); 46 | } 47 | Ok(response) 48 | } 49 | })) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/http/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use self::{body::Body, request::Request, response::Response}; 2 | 3 | pub mod body; 4 | pub mod header; 5 | pub mod idle; 6 | pub mod layer; 7 | pub mod middleware; 8 | pub mod request; 9 | pub mod response; 10 | pub mod sse; 11 | 12 | pub type Error = Box; 13 | 14 | pub type Result = std::result::Result; 15 | -------------------------------------------------------------------------------- /packages/http/src/middleware.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/ignore/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tangram_ignore" 3 | authors.workspace = true 4 | description.workspace = true 5 | edition.workspace = true 6 | homepage.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | rust-version.workspace = true 10 | version.workspace = true 11 | 12 | [lints] 13 | workspace = true 14 | 15 | [dev-dependencies] 16 | indoc = { workspace = true } 17 | pretty_assertions = { workspace = true } 18 | tangram_temp = { workspace = true } 19 | 20 | [dependencies] 21 | derive_more = { workspace = true } 22 | fnv = { workspace = true } 23 | globset = { workspace = true } 24 | tokio = { workspace = true } 25 | -------------------------------------------------------------------------------- /packages/ignore/src/tests.rs: -------------------------------------------------------------------------------- 1 | use super::Ignorer; 2 | use indoc::indoc; 3 | use pretty_assertions::assert_eq; 4 | use tangram_temp::{self as temp, Temp}; 5 | 6 | #[tokio::test] 7 | async fn test() { 8 | let temp = Temp::new(); 9 | let artifact = temp::Artifact::from(temp::directory! { 10 | ".DS_Store" => temp::file!(""), 11 | ".gitignore" => temp::file!(indoc!(" 12 | foo 13 | foo.txt 14 | ")), 15 | "foo" => temp::directory! { 16 | "foo.txt" => temp::file!(""), 17 | }, 18 | "bar" => temp::directory! { 19 | "bar.txt" => temp::file!(""), 20 | }, 21 | "foo.txt" => temp::file!(""), 22 | "bar.txt" => temp::file!(""), 23 | "directory" => temp::directory! {}, 24 | }); 25 | artifact.to_path(temp.path()).await.unwrap(); 26 | let file_names = vec![".gitignore".into()]; 27 | let global = indoc!( 28 | " 29 | .DS_Store 30 | " 31 | ); 32 | let mut matcher = Ignorer::new(file_names, Some(global)).unwrap(); 33 | let right = vec![ 34 | (".DS_Store", true), 35 | (".gitignore", false), 36 | ("foo", true), 37 | ("foo/foo.txt", true), 38 | ("bar", false), 39 | ("bar/bar.txt", false), 40 | ("foo.txt", true), 41 | ("bar.txt", false), 42 | ]; 43 | let mut left = Vec::new(); 44 | for (path, _) in &right { 45 | let matches = matcher.matches(&temp.path().join(path), None).unwrap(); 46 | left.push((*path, matches)); 47 | } 48 | assert_eq!(left, right); 49 | } 50 | -------------------------------------------------------------------------------- /packages/messenger/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tangram_messenger" 3 | 4 | description = { workspace = true } 5 | edition = { workspace = true } 6 | homepage = { workspace = true } 7 | license = { workspace = true } 8 | publish = false 9 | repository = { workspace = true } 10 | rust-version = { workspace = true } 11 | version = { workspace = true } 12 | 13 | [lints] 14 | workspace = true 15 | 16 | [dependencies] 17 | async-broadcast = { workspace = true } 18 | async-channel = { workspace = true } 19 | crossbeam = { workspace = true } 20 | async-nats = { workspace = true } 21 | bytes = { workspace = true } 22 | dashmap = { workspace = true } 23 | derive_more = { workspace = true } 24 | fnv = { workspace = true } 25 | futures = { workspace = true } 26 | num = { workspace = true } 27 | uuid = { workspace = true } 28 | tangram_either = { workspace = true } 29 | tokio = { workspace = true } 30 | -------------------------------------------------------------------------------- /packages/messenger/src/acker.rs: -------------------------------------------------------------------------------- 1 | use crate::Error; 2 | use futures::{FutureExt as _, future::BoxFuture}; 3 | 4 | #[derive(Default)] 5 | pub struct Acker { 6 | ack: Option>>, 7 | } 8 | 9 | impl Acker { 10 | pub fn new(ack: impl Future> + Send + 'static) -> Self { 11 | Self { 12 | ack: Some(ack.boxed()), 13 | } 14 | } 15 | 16 | pub async fn ack(mut self) -> Result<(), Error> { 17 | if let Some(fut) = self.ack.take() { 18 | fut.await?; 19 | } 20 | Ok(()) 21 | } 22 | } 23 | 24 | impl Drop for Acker { 25 | fn drop(&mut self) { 26 | drop(self.ack.take()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tangramdotdev/runtime", 3 | "private": true, 4 | "scripts": { 5 | "check": "tsc && biome check src", 6 | "format": "biome check --write tangram.d.ts src" 7 | }, 8 | "type": "module", 9 | "dependencies": { 10 | "immutable": "^4.3.7" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/runtime/src/artifact.ts: -------------------------------------------------------------------------------- 1 | import * as tg from "./index.ts"; 2 | 3 | export type Artifact = tg.Directory | tg.File | tg.Symlink; 4 | 5 | export namespace Artifact { 6 | export type Id = string; 7 | 8 | export let withId = (id: Artifact.Id): Artifact => { 9 | let prefix = id.substring(0, 3); 10 | if (prefix === "dir") { 11 | return tg.Directory.withId(id); 12 | } else if (prefix === "fil") { 13 | return tg.File.withId(id); 14 | } else if (prefix === "sym") { 15 | return tg.Symlink.withId(id); 16 | } else { 17 | throw new Error(`invalid artifact id: ${id}`); 18 | } 19 | }; 20 | 21 | export let is = (value: unknown): value is Artifact => { 22 | return ( 23 | value instanceof tg.Directory || 24 | value instanceof tg.File || 25 | value instanceof tg.Symlink 26 | ); 27 | }; 28 | 29 | export let expect = (value: unknown): Artifact => { 30 | tg.assert(is(value)); 31 | return value; 32 | }; 33 | 34 | export let assert = (value: unknown): asserts value is Artifact => { 35 | tg.assert(is(value)); 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /packages/runtime/src/assert.ts: -------------------------------------------------------------------------------- 1 | export let assert: (condition: unknown, message?: string) => asserts condition = 2 | (condition, message) => { 3 | if (!condition) { 4 | throw new Error(message ?? "failed assertion"); 5 | } 6 | }; 7 | 8 | export let unimplemented = (message?: string): never => { 9 | throw new Error(message ?? "reached unimplemented code"); 10 | }; 11 | 12 | export let unreachable = (message?: string): never => { 13 | throw new Error(message ?? "reached unreachable code"); 14 | }; 15 | 16 | export let todo = (): never => { 17 | throw new Error("reached todo"); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/runtime/src/error.ts: -------------------------------------------------------------------------------- 1 | import type * as tg from "./index.ts"; 2 | 3 | export class Error_ { 4 | message: string; 5 | location: Location | undefined; 6 | stack: Array | undefined; 7 | source: Source | undefined; 8 | values: Map | undefined; 9 | 10 | constructor( 11 | message: string, 12 | location?: Location, 13 | stack?: Array, 14 | source?: Source, 15 | values?: Map, 16 | ) { 17 | this.message = message; 18 | this.location = location; 19 | this.stack = stack; 20 | this.source = source; 21 | this.values = values; 22 | } 23 | } 24 | 25 | type Location = { 26 | symbol?: string; 27 | file: File; 28 | line: number; 29 | column: number; 30 | }; 31 | 32 | type File = 33 | | { kind: "internal"; value: string } 34 | | { kind: "external"; value: tg.Module }; 35 | 36 | type Source = { 37 | error: Error_; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/runtime/src/main.ts: -------------------------------------------------------------------------------- 1 | import * as tg from "./index.ts"; 2 | 3 | let console = { log: tg.log, error: tg.error }; 4 | Object.defineProperties(globalThis, { 5 | console: { 6 | value: console, 7 | configurable: true, 8 | enumerable: true, 9 | writable: true, 10 | }, 11 | }); 12 | 13 | let Tangram = tg.template; 14 | Object.assign(Tangram, tg); 15 | Object.defineProperty(Tangram, "process", { get: () => tg.Process.current }); 16 | Object.defineProperties(globalThis, { 17 | Tangram: { value: Tangram }, 18 | tg: { value: Tangram }, 19 | }); 20 | -------------------------------------------------------------------------------- /packages/runtime/src/module.ts: -------------------------------------------------------------------------------- 1 | import type * as tg from "./index.ts"; 2 | 3 | export type Module = { 4 | kind: Module.Kind; 5 | referent: tg.Referent; 6 | }; 7 | 8 | export namespace Module { 9 | export type Data = { 10 | kind: Module.Kind; 11 | referent: tg.Referent; 12 | }; 13 | 14 | export type Item = string | tg.Object; 15 | 16 | export type Kind = 17 | | "js" 18 | | "ts" 19 | | "dts" 20 | | "object" 21 | | "artifact" 22 | | "blob" 23 | | "directory" 24 | | "file" 25 | | "symlink" 26 | | "graph" 27 | | "command"; 28 | } 29 | -------------------------------------------------------------------------------- /packages/runtime/src/reference.ts: -------------------------------------------------------------------------------- 1 | export type Reference = string; 2 | -------------------------------------------------------------------------------- /packages/runtime/src/referent.ts: -------------------------------------------------------------------------------- 1 | import type * as tg from "./index.ts"; 2 | 3 | export type Referent = { 4 | item: T; 5 | path?: string | undefined; 6 | subpath?: string | undefined; 7 | tag?: tg.Tag | undefined; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/runtime/src/sleep.ts: -------------------------------------------------------------------------------- 1 | export let sleep = async (duration: number) => { 2 | return await syscall("sleep", duration); 3 | }; 4 | -------------------------------------------------------------------------------- /packages/runtime/src/start.ts: -------------------------------------------------------------------------------- 1 | import * as tg from "./index.ts"; 2 | 3 | export let start = async (process: tg.Process): Promise => { 4 | // Load the process and the command. 5 | await process.load(); 6 | const command = process.state!.command; 7 | await command.load(); 8 | 9 | // Set the current process. 10 | tg.Process.current = process; 11 | 12 | // Import the module. 13 | // @ts-ignore 14 | // biome-ignore lint/security/noGlobalEval: 15 | let namespace = await eval(`import("!")`); 16 | 17 | // Get the export. 18 | let executable = await command.executable(); 19 | tg.assert("module" in executable); 20 | let export_ = executable.export; 21 | if (export_ === undefined) { 22 | throw new Error("the executable must have an export"); 23 | } 24 | 25 | // Get the output. 26 | let output: tg.Value; 27 | if (!(export_ in namespace)) { 28 | throw new Error("failed to find the export"); 29 | } 30 | let value = await namespace[export_]; 31 | if (tg.Value.is(value)) { 32 | output = value; 33 | } else if (typeof value === "function") { 34 | let args = await command.args(); 35 | output = await tg.resolve(value(...args)); 36 | } else { 37 | throw new Error("the export must be a tg.Value or a function"); 38 | } 39 | 40 | return output; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/runtime/src/tag.ts: -------------------------------------------------------------------------------- 1 | export type Tag = string; 2 | -------------------------------------------------------------------------------- /packages/runtime/src/util.ts: -------------------------------------------------------------------------------- 1 | import type * as tg from "./index.ts"; 2 | 3 | export type MaybePromise = T | Promise; 4 | 5 | export type MaybeMutation = T | tg.Mutation; 6 | 7 | export type MutationMap< 8 | T extends { [key: string]: tg.Value } = { [key: string]: tg.Value }, 9 | > = { 10 | [K in keyof T]?: tg.Mutation; 11 | }; 12 | 13 | export type MaybeMutationMap< 14 | T extends { [key: string]: tg.Value } = { [key: string]: tg.Value }, 15 | > = { 16 | [K in keyof T]?: tg.MaybeMutation; 17 | }; 18 | 19 | export type ValueOrMaybeMutationMap = T extends 20 | | undefined 21 | | boolean 22 | | number 23 | | string 24 | | tg.Object 25 | | Uint8Array 26 | | tg.Mutation 27 | | tg.Template 28 | | Array 29 | ? T 30 | : T extends { [key: string]: tg.Value } 31 | ? { 32 | [K in keyof T]?: T[K] | tg.Mutation; 33 | } 34 | : never; 35 | 36 | export type UnresolvedArgs> = { 37 | [K in keyof T]: tg.Unresolved; 38 | }; 39 | 40 | export type ResolvedArgs>> = { 41 | [K in keyof T]: tg.Resolved; 42 | }; 43 | 44 | export type ReturnValue = tg.MaybePromise | tg.Unresolved; 45 | 46 | export type ResolvedReturnValue = 47 | T extends tg.MaybePromise 48 | ? undefined 49 | : T extends tg.Unresolved 50 | ? tg.Resolved 51 | : never; 52 | -------------------------------------------------------------------------------- /packages/runtime/src/value.ts: -------------------------------------------------------------------------------- 1 | import * as tg from "./index.ts"; 2 | 3 | export type Value = 4 | | undefined 5 | | boolean 6 | | number 7 | | string 8 | | Array 9 | | { [key: string]: Value } 10 | | tg.Object 11 | | Uint8Array 12 | | tg.Mutation 13 | | tg.Template; 14 | 15 | export namespace Value { 16 | export let is = (value: unknown): value is Value => { 17 | return ( 18 | value === undefined || 19 | typeof value === "boolean" || 20 | typeof value === "number" || 21 | typeof value === "string" || 22 | value instanceof Array || 23 | tg.Value.isMap(value) || 24 | tg.Object.is(value) || 25 | value instanceof Uint8Array || 26 | value instanceof tg.Mutation || 27 | value instanceof tg.Template 28 | ); 29 | }; 30 | 31 | export let expect = (value: unknown): Value => { 32 | tg.assert(is(value)); 33 | return value; 34 | }; 35 | 36 | export let assert = (value: unknown): asserts value is Value => { 37 | tg.assert(is(value)); 38 | }; 39 | 40 | export let isMap = (value: unknown): value is { [key: string]: Value } => { 41 | if ( 42 | !(typeof value === "object" && value !== null) || 43 | value instanceof Array || 44 | value instanceof Uint8Array || 45 | value instanceof tg.Mutation || 46 | value instanceof tg.Template || 47 | tg.Object.is(value) 48 | ) { 49 | return false; 50 | } 51 | return Object.entries(value as object).every(([_, val]) => { 52 | return Value.is(val); 53 | }); 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /packages/runtime/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowImportingTsExtensions": true, 4 | "exactOptionalPropertyTypes": true, 5 | "isolatedModules": true, 6 | "lib": ["esnext"], 7 | "module": "esnext", 8 | "moduleResolution": "bundler", 9 | "noEmit": true, 10 | "noUncheckedIndexedAccess": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "strict": true, 14 | "target": "esnext", 15 | "types": [] 16 | }, 17 | "exclude": ["tangram.d.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/sandbox/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tangram_sandbox" 3 | authors.workspace = true 4 | description.workspace = true 5 | edition.workspace = true 6 | homepage.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | rust-version.workspace = true 10 | version.workspace = true 11 | 12 | [dependencies] 13 | bytes = { workspace = true } 14 | dashmap = { workspace = true } 15 | indoc = { workspace = true } 16 | libc = { workspace = true } 17 | num = { workspace = true } 18 | scopeguard = { workspace = true } 19 | tangram_temp = { workspace = true } 20 | tangram_either = { workspace = true } 21 | tokio = { workspace = true } 22 | tokio-util = { workspace = true } 23 | 24 | [dev-dependencies] 25 | futures = { workspace = true } 26 | tangram_futures = { workspace = true } 27 | 28 | [lints] 29 | workspace = true 30 | -------------------------------------------------------------------------------- /packages/server/src/blob.rs: -------------------------------------------------------------------------------- 1 | pub use self::read::Reader; 2 | 3 | pub(crate) mod create; 4 | mod read; 5 | -------------------------------------------------------------------------------- /packages/server/src/check.rs: -------------------------------------------------------------------------------- 1 | use crate::{Server, compiler::Compiler}; 2 | use tangram_client as tg; 3 | use tangram_http::{Body, request::Ext as _, response::builder::Ext as _}; 4 | 5 | impl Server { 6 | pub async fn check(&self, mut arg: tg::check::Arg) -> tg::Result { 7 | // If the remote arg is set, then forward the request. 8 | if let Some(remote) = arg.remote.take() { 9 | let remote = self.get_remote_client(remote).await?; 10 | let arg = tg::check::Arg { 11 | remote: None, 12 | ..arg 13 | }; 14 | let output = remote.check(arg).await?; 15 | return Ok(output); 16 | } 17 | 18 | // Create the compiler. 19 | let compiler = Compiler::new(self, tokio::runtime::Handle::current()); 20 | 21 | // Create the module. 22 | let module = arg.package; 23 | 24 | // Check the package. 25 | let diagnostics = compiler.check(vec![module]).await?; 26 | 27 | // Create the output. 28 | let output = tg::check::Output { diagnostics }; 29 | 30 | // Stop and await the compiler. 31 | compiler.stop(); 32 | compiler.wait().await; 33 | 34 | Ok(output) 35 | } 36 | 37 | pub(crate) async fn handle_check_request( 38 | handle: &H, 39 | request: http::Request, 40 | ) -> tg::Result> 41 | where 42 | H: tg::Handle, 43 | { 44 | let arg = request.json().await?; 45 | let output = handle.check(arg).await?; 46 | let response = http::Response::builder().json(output).unwrap(); 47 | Ok(response) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/server/src/compiler/check.rs: -------------------------------------------------------------------------------- 1 | use super::Compiler; 2 | use tangram_client as tg; 3 | 4 | #[derive(Debug, serde::Serialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct Request { 7 | pub modules: Vec, 8 | } 9 | 10 | #[derive(Debug, serde::Deserialize)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct Response { 13 | pub diagnostics: Vec, 14 | } 15 | 16 | impl Compiler { 17 | /// Get all diagnostics for the provided modules. 18 | pub async fn check(&self, modules: Vec) -> tg::Result> { 19 | // Create the request. 20 | let request = super::Request::Check(Request { modules }); 21 | 22 | // Perform the request. 23 | let response = self.request(request).await?.unwrap_check(); 24 | 25 | Ok(response.diagnostics) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/server/src/compiler/format.rs: -------------------------------------------------------------------------------- 1 | use super::Compiler; 2 | use lsp_types as lsp; 3 | use tangram_client as tg; 4 | 5 | impl Compiler { 6 | pub fn format(text: &str) -> tg::Result { 7 | let source_type = biome_js_syntax::JsFileSource::ts(); 8 | let options = biome_js_parser::JsParserOptions::default(); 9 | let node = biome_js_parser::parse(text, source_type, options); 10 | let options = biome_js_formatter::context::JsFormatOptions::new(source_type); 11 | let formatted = biome_js_formatter::format_node(options, &node.syntax()) 12 | .map_err(|source| tg::error!(!source, "failed to format"))?; 13 | let text = formatted 14 | .print() 15 | .map_err(|source| tg::error!(!source, "failed to format"))? 16 | .into_code(); 17 | Ok(text) 18 | } 19 | } 20 | 21 | impl Compiler { 22 | pub(super) async fn handle_format_request( 23 | &self, 24 | params: lsp::DocumentFormattingParams, 25 | ) -> tg::Result>> { 26 | // Get the module. 27 | let module = self.module_for_lsp_uri(¶ms.text_document.uri).await?; 28 | 29 | // Load the module. 30 | let text = self.load_module(&module).await?; 31 | 32 | // Get the text range. 33 | let range = tg::Range::from_byte_range_in_string(&text, 0..text.len()); 34 | 35 | // Format the text. 36 | let formatted_text = Self::format(&text)?; 37 | 38 | // Create the edit. 39 | let edit = lsp::TextEdit { 40 | range: range.into(), 41 | new_text: formatted_text, 42 | }; 43 | 44 | Ok(Some(vec![edit])) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/server/src/compiler/parse.rs: -------------------------------------------------------------------------------- 1 | use super::Compiler; 2 | use std::rc::Rc; 3 | use swc_core as swc; 4 | use tangram_client as tg; 5 | 6 | pub struct Output { 7 | pub program: swc::ecma::ast::Program, 8 | pub source_map: Rc, 9 | } 10 | 11 | impl Compiler { 12 | /// Parse a module. 13 | pub fn parse_module(text: String) -> tg::Result { 14 | // Create the parser. 15 | let syntax = swc::ecma::parser::TsSyntax::default(); 16 | let syntax = swc::ecma::parser::Syntax::Typescript(syntax); 17 | let source_map = Rc::new(swc::common::SourceMap::default()); 18 | let source_file = source_map.new_source_file(swc::common::FileName::Anon.into(), text); 19 | let input = swc::ecma::parser::StringInput::from(&*source_file); 20 | let mut parser = swc::ecma::parser::Parser::new(syntax, input, None); 21 | 22 | // Parse the text. 23 | let program = parser 24 | .parse_program() 25 | .map_err(|error| tg::error!("{}", error.into_kind().msg()))?; 26 | 27 | Ok(Output { 28 | program, 29 | source_map, 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/server/src/compiler/syscall/document.rs: -------------------------------------------------------------------------------- 1 | use crate::compiler::Compiler; 2 | use tangram_client as tg; 3 | 4 | pub fn list( 5 | _scope: &mut v8::HandleScope, 6 | compiler: &Compiler, 7 | _args: (), 8 | ) -> tg::Result> { 9 | compiler 10 | .main_runtime_handle 11 | .clone() 12 | .block_on(async move { Ok(compiler.list_documents().await) }) 13 | } 14 | -------------------------------------------------------------------------------- /packages/server/src/compiler/syscall/log.rs: -------------------------------------------------------------------------------- 1 | use crate::compiler::Compiler; 2 | use tangram_client as tg; 3 | 4 | pub fn log(_scope: &mut v8::HandleScope, _compiler: &Compiler, args: (String,)) -> tg::Result<()> { 5 | let (string,) = args; 6 | tracing::info!("{string}"); 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /packages/server/src/document.rs: -------------------------------------------------------------------------------- 1 | use crate::{Server, compiler::Compiler}; 2 | use tangram_client as tg; 3 | use tangram_http::{Body, request::Ext as _, response::builder::Ext as _}; 4 | 5 | impl Server { 6 | pub async fn document(&self, mut arg: tg::document::Arg) -> tg::Result { 7 | // If the remote arg is set, then forward the request. 8 | if let Some(remote) = arg.remote.take() { 9 | let remote = self.get_remote_client(remote.clone()).await?; 10 | let arg = tg::document::Arg { 11 | remote: None, 12 | ..arg 13 | }; 14 | let output = remote.document(arg).await?; 15 | return Ok(output); 16 | } 17 | 18 | // Create the compiler. 19 | let compiler = Compiler::new(self, tokio::runtime::Handle::current()); 20 | 21 | // Document the module. 22 | let output = compiler.document(&arg.module).await?; 23 | 24 | // Stop and await the compiler. 25 | compiler.stop(); 26 | compiler.wait().await; 27 | 28 | Ok(output) 29 | } 30 | 31 | pub(crate) async fn handle_document_request( 32 | handle: &H, 33 | request: http::Request, 34 | ) -> tg::Result> 35 | where 36 | H: tg::Handle, 37 | { 38 | let arg = request.json().await?; 39 | let output = handle.document(arg).await?; 40 | let response = http::Response::builder().json(output).unwrap(); 41 | Ok(response) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/server/src/messenger.rs: -------------------------------------------------------------------------------- 1 | use tangram_either::Either; 2 | 3 | pub type Messenger = 4 | Either; 5 | -------------------------------------------------------------------------------- /packages/server/src/object.rs: -------------------------------------------------------------------------------- 1 | mod complete; 2 | mod get; 3 | mod metadata; 4 | mod put; 5 | mod touch; 6 | -------------------------------------------------------------------------------- /packages/server/src/object/complete.rs: -------------------------------------------------------------------------------- 1 | use crate::Server; 2 | use indoc::formatdoc; 3 | use tangram_client as tg; 4 | use tangram_database::{self as db, Database as _, Query as _}; 5 | 6 | impl Server { 7 | pub(crate) async fn try_get_object_complete_local( 8 | &self, 9 | id: &tg::object::Id, 10 | ) -> tg::Result> { 11 | // Get an index connection. 12 | let connection = self 13 | .index 14 | .connection() 15 | .await 16 | .map_err(|source| tg::error!(!source, "failed to get a database connection"))?; 17 | 18 | // Get the object metadata. 19 | let p = connection.p(); 20 | let statement = formatdoc!( 21 | " 22 | select complete 23 | from objects 24 | where id = {p}1; 25 | ", 26 | ); 27 | let params = db::params![id]; 28 | let output = connection 29 | .query_optional_into(statement.into(), params) 30 | .await 31 | .map_err(|source| tg::error!(!source, "failed to execute the statement"))?; 32 | 33 | // Drop the database connection. 34 | drop(connection); 35 | 36 | Ok(output) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/server/src/pipe.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use tangram_client as tg; 3 | use tokio::sync::Mutex; 4 | 5 | mod close; 6 | mod create; 7 | mod read; 8 | mod write; 9 | 10 | pub(crate) struct Pipe { 11 | pub host: Arc>, 12 | pub guest: std::os::unix::net::UnixStream, 13 | } 14 | 15 | impl Pipe { 16 | pub async fn open() -> tg::Result { 17 | let (host, guest) = tokio::net::UnixStream::pair() 18 | .map_err(|source| tg::error!(!source, "failed to open pipe"))?; 19 | let guest = guest 20 | .into_std() 21 | .map_err(|source| tg::error!(!source, "failed to open pipe"))?; 22 | guest 23 | .set_nonblocking(false) 24 | .map_err(|source| tg::error!(!source, "failed to open pipe"))?; 25 | let host = Arc::new(Mutex::new(host)); 26 | Ok(Self { host, guest }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/server/src/pipe/close.rs: -------------------------------------------------------------------------------- 1 | use crate::Server; 2 | use tangram_client as tg; 3 | use tangram_http::{Body, request::Ext as _, response::builder::Ext as _}; 4 | 5 | impl Server { 6 | pub async fn close_pipe(&self, id: &tg::pipe::Id, arg: tg::pipe::close::Arg) -> tg::Result<()> { 7 | if let Some(remote) = arg.remote { 8 | let remote = self.get_remote_client(remote).await?; 9 | let arg = tg::pipe::close::Arg::default(); 10 | remote.close_pipe(id, arg).await?; 11 | return Ok(()); 12 | } 13 | self.pipes.remove(id); 14 | Ok(()) 15 | } 16 | 17 | pub(crate) async fn handle_close_pipe_request( 18 | handle: &H, 19 | request: http::Request, 20 | id: &str, 21 | ) -> tg::Result> 22 | where 23 | H: tg::Handle, 24 | { 25 | let id = id.parse()?; 26 | let arg = request.query_params().transpose()?.unwrap_or_default(); 27 | handle.close_pipe(&id, arg).await?; 28 | let response = http::Response::builder().empty().unwrap(); 29 | Ok(response) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/server/src/pipe/create.rs: -------------------------------------------------------------------------------- 1 | use crate::Server; 2 | use tangram_client as tg; 3 | use tangram_http::{Body, request::Ext as _, response::builder::Ext as _}; 4 | impl Server { 5 | pub async fn create_pipe( 6 | &self, 7 | mut arg: tg::pipe::create::Arg, 8 | ) -> tg::Result { 9 | if let Some(remote) = arg.remote.take() { 10 | let remote = self.get_remote_client(remote).await?; 11 | return remote.create_pipe(arg).await; 12 | } 13 | 14 | // Create the pipe. 15 | let id = tg::pipe::Id::new(); 16 | let pipe = super::Pipe::open().await?; 17 | self.pipes.insert(id.clone(), pipe); 18 | 19 | // Return the ID. 20 | let output = tg::pipe::create::Output { id }; 21 | Ok(output) 22 | } 23 | 24 | pub(crate) async fn handle_create_pipe_request( 25 | handle: &H, 26 | request: http::Request, 27 | ) -> tg::Result> 28 | where 29 | H: tg::Handle, 30 | { 31 | let arg = request.json().await?; 32 | let output = handle.create_pipe(arg).await?; 33 | let response = http::Response::builder().json(output).unwrap(); 34 | Ok(response) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/server/src/process.rs: -------------------------------------------------------------------------------- 1 | use crate::Server; 2 | use indoc::formatdoc; 3 | use tangram_client as tg; 4 | use tangram_database::{self as db, prelude::*}; 5 | 6 | pub(crate) mod children; 7 | pub(crate) mod dequeue; 8 | pub(crate) mod finish; 9 | pub(crate) mod get; 10 | pub(crate) mod heartbeat; 11 | pub(crate) mod log; 12 | pub(crate) mod metadata; 13 | pub(crate) mod put; 14 | pub(crate) mod signal; 15 | pub(crate) mod spawn; 16 | pub(crate) mod start; 17 | pub(crate) mod status; 18 | pub(crate) mod touch; 19 | pub(crate) mod wait; 20 | 21 | impl Server { 22 | pub(crate) async fn get_process_exists_local(&self, id: &tg::process::Id) -> tg::Result { 23 | // Get a database connection. 24 | let connection = self 25 | .database 26 | .connection() 27 | .await 28 | .map_err(|source| tg::error!(!source, "failed to get a database connection"))?; 29 | 30 | // Check if the process exists. 31 | let p = connection.p(); 32 | let statement = formatdoc!( 33 | " 34 | select count(*) != 0 35 | from processes 36 | where id = {p}1; 37 | " 38 | ); 39 | let params = db::params![id]; 40 | let exists = connection 41 | .query_one_value_into(statement.into(), params) 42 | .await 43 | .map_err(|source| tg::error!(!source, "failed to execute the statement"))?; 44 | 45 | // Drop the database connection. 46 | drop(connection); 47 | 48 | Ok(exists) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/server/src/process/children.rs: -------------------------------------------------------------------------------- 1 | mod get; 2 | -------------------------------------------------------------------------------- /packages/server/src/process/log.rs: -------------------------------------------------------------------------------- 1 | pub mod get; 2 | pub mod post; 3 | pub mod reader; 4 | -------------------------------------------------------------------------------- /packages/server/src/process/signal.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod get; 2 | pub(crate) mod post; 3 | -------------------------------------------------------------------------------- /packages/server/src/process/signal/post.rs: -------------------------------------------------------------------------------- 1 | use crate::Server; 2 | use tangram_client as tg; 3 | use tangram_http::{Body, request::Ext as _, response::builder::Ext as _}; 4 | use tangram_messenger::prelude::*; 5 | 6 | impl Server { 7 | pub(crate) async fn post_process_signal( 8 | &self, 9 | id: &tg::process::Id, 10 | mut arg: tg::process::signal::post::Arg, 11 | ) -> tg::Result<()> { 12 | if let Some(remote) = arg.remote.take() { 13 | let remote = self.get_remote_client(remote).await?; 14 | return remote.post_process_signal(id, arg).await; 15 | } 16 | let payload = serde_json::to_vec(&tg::process::signal::get::Event::Signal(arg.signal)) 17 | .unwrap() 18 | .into(); 19 | self.messenger 20 | .publish(format!("processes.{id}.signal"), payload) 21 | .await 22 | .map_err(|source| tg::error!(!source, "failed to signal the process"))?; 23 | Ok(()) 24 | } 25 | 26 | pub(crate) async fn handle_post_process_signal_request( 27 | handle: &H, 28 | request: http::Request, 29 | id: &str, 30 | ) -> tg::Result> 31 | where 32 | H: tg::Handle, 33 | { 34 | let id = id 35 | .parse() 36 | .map_err(|source| tg::error!(!source, "failed to parse process id"))?; 37 | let arg = request 38 | .json() 39 | .await 40 | .map_err(|_| tg::error!("failed to deserialize the arg"))?; 41 | handle 42 | .signal_process(&id, arg) 43 | .await 44 | .map_err(|source| tg::error!(!source, "failed to post process signal"))?; 45 | let response = http::Response::builder().empty().unwrap(); 46 | Ok(response) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/server/src/pty.rs: -------------------------------------------------------------------------------- 1 | use std::os::fd::{FromRawFd, OwnedFd}; 2 | use tangram_client as tg; 3 | 4 | mod close; 5 | mod create; 6 | mod read; 7 | mod size; 8 | mod write; 9 | 10 | pub(crate) struct Pty { 11 | pub host: OwnedFd, 12 | pub guest: OwnedFd, 13 | } 14 | 15 | impl Pty { 16 | async fn open(size: tg::pty::Size) -> tg::Result { 17 | tokio::task::spawn_blocking(move || unsafe { 18 | let mut win_size = libc::winsize { 19 | ws_col: size.cols, 20 | ws_row: size.rows, 21 | ws_xpixel: 0, 22 | ws_ypixel: 0, 23 | }; 24 | let mut host = 0; 25 | let mut guest = 0; 26 | let mut tty_name = [0; 256]; 27 | if libc::openpty( 28 | std::ptr::addr_of_mut!(host), 29 | std::ptr::addr_of_mut!(guest), 30 | tty_name.as_mut_ptr(), 31 | std::ptr::null_mut(), 32 | std::ptr::addr_of_mut!(win_size), 33 | ) < 0 34 | { 35 | return Err(std::io::Error::last_os_error()); 36 | } 37 | 38 | // Mark pty as non blocking. 39 | let flags = libc::fcntl(host, libc::F_GETFL); 40 | if flags < 0 { 41 | return Err(std::io::Error::last_os_error()); 42 | } 43 | let flags = flags | libc::O_NONBLOCK; 44 | let ret = libc::fcntl(host, libc::F_SETFL, flags); 45 | if ret < 0 { 46 | return Err(std::io::Error::last_os_error()); 47 | } 48 | 49 | let host = OwnedFd::from_raw_fd(host); 50 | let guest = OwnedFd::from_raw_fd(guest); 51 | let pty = Self { host, guest }; 52 | Ok(pty) 53 | }) 54 | .await 55 | .unwrap() 56 | .map_err(|source| tg::error!(!source, "failed to open pty")) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/server/src/pty/close.rs: -------------------------------------------------------------------------------- 1 | use crate::Server; 2 | use tangram_client as tg; 3 | use tangram_http::{Body, request::Ext as _, response::builder::Ext as _}; 4 | 5 | impl Server { 6 | pub async fn close_pty(&self, id: &tg::pty::Id, arg: tg::pty::close::Arg) -> tg::Result<()> { 7 | if let Some(remote) = arg.remote { 8 | let remote = self.get_remote_client(remote).await?; 9 | let arg = tg::pty::close::Arg::default(); 10 | remote.close_pty(id, arg).await?; 11 | return Ok(()); 12 | } 13 | 14 | self.ptys.remove(id); 15 | Ok(()) 16 | } 17 | 18 | pub(crate) async fn handle_close_pty_request( 19 | handle: &H, 20 | request: http::Request, 21 | id: &str, 22 | ) -> tg::Result> 23 | where 24 | H: tg::Handle, 25 | { 26 | let id = id.parse()?; 27 | 28 | // Get the query. 29 | let arg = request.query_params().transpose()?.unwrap_or_default(); 30 | 31 | handle.close_pty(&id, arg).await?; 32 | let response = http::Response::builder().empty().unwrap(); 33 | Ok(response) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/server/src/pty/create.rs: -------------------------------------------------------------------------------- 1 | use crate::Server; 2 | use tangram_client as tg; 3 | use tangram_http::{Body, request::Ext as _, response::builder::Ext as _}; 4 | 5 | impl Server { 6 | pub async fn create_pty( 7 | &self, 8 | mut arg: tg::pty::create::Arg, 9 | ) -> tg::Result { 10 | if let Some(remote) = arg.remote.take() { 11 | let remote = self.get_remote_client(remote).await?; 12 | return remote.create_pty(arg).await; 13 | } 14 | let id = tg::pty::Id::new(); 15 | 16 | // Create the pty. 17 | let pty = super::Pty::open(arg.size).await?; 18 | self.ptys.insert(id.clone(), pty); 19 | 20 | // Return the id. 21 | let output = tg::pty::create::Output { id }; 22 | Ok(output) 23 | } 24 | 25 | pub(crate) async fn handle_create_pty_request( 26 | handle: &H, 27 | request: http::Request, 28 | ) -> tg::Result> 29 | where 30 | H: tg::Handle, 31 | { 32 | let arg = request.json().await?; 33 | let output = handle.create_pty(arg).await?; 34 | let response = http::Response::builder().json(output).unwrap(); 35 | Ok(response) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/server/src/reference.rs: -------------------------------------------------------------------------------- 1 | mod get; 2 | -------------------------------------------------------------------------------- /packages/server/src/remote.rs: -------------------------------------------------------------------------------- 1 | use crate::Server; 2 | use futures::{TryStreamExt as _, stream::FuturesUnordered}; 3 | use std::collections::BTreeMap; 4 | use tangram_client as tg; 5 | 6 | pub mod delete; 7 | pub mod get; 8 | pub mod list; 9 | pub mod put; 10 | 11 | impl Server { 12 | pub async fn get_remote_clients(&self) -> tg::Result> { 13 | let output = self.list_remotes(tg::remote::list::Arg::default()).await?; 14 | let remotes = output 15 | .data 16 | .into_iter() 17 | .map(|output| async { 18 | let client = self.get_remote_client(output.name.clone()).await?; 19 | Ok::<_, tg::Error>((output.name, client)) 20 | }) 21 | .collect::>() 22 | .try_collect() 23 | .await?; 24 | Ok(remotes) 25 | } 26 | 27 | pub async fn get_remote_client(&self, remote: String) -> tg::Result { 28 | self.try_get_remote_client(remote) 29 | .await? 30 | .ok_or_else(|| tg::error!("failed to find the remote")) 31 | } 32 | 33 | pub async fn try_get_remote_client(&self, remote: String) -> tg::Result> { 34 | if let Some(client) = self.remotes.get(&remote) { 35 | return Ok(Some(client.clone())); 36 | } 37 | let Some(output) = self.try_get_remote(&remote).await? else { 38 | return Ok(None); 39 | }; 40 | let client = tg::Client::new(output.url, Some(self.version.clone())); 41 | self.remotes.insert(remote, client.clone()); 42 | Ok(Some(client)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/server/src/remote/delete.rs: -------------------------------------------------------------------------------- 1 | use crate::Server; 2 | use indoc::formatdoc; 3 | use tangram_client as tg; 4 | use tangram_database::{self as db, prelude::*}; 5 | use tangram_http::{Body, response::builder::Ext as _}; 6 | 7 | impl Server { 8 | pub async fn delete_remote(&self, name: &str) -> tg::Result<()> { 9 | let connection = self 10 | .database 11 | .write_connection() 12 | .await 13 | .map_err(|source| tg::error!(!source, "failed to get a database connection"))?; 14 | let p = connection.p(); 15 | let statement = formatdoc!( 16 | " 17 | delete from remotes 18 | where name = {p}1; 19 | ", 20 | ); 21 | let params = db::params![&name]; 22 | connection 23 | .execute(statement.into(), params) 24 | .await 25 | .map_err(|source| tg::error!(!source, "failed to execute the statemtent"))?; 26 | Ok(()) 27 | } 28 | 29 | pub(crate) async fn handle_delete_remote_request( 30 | handle: &H, 31 | _request: http::Request, 32 | name: &str, 33 | ) -> tg::Result> 34 | where 35 | H: tg::Handle, 36 | { 37 | handle.delete_remote(name).await?; 38 | let response = http::Response::builder().empty().unwrap(); 39 | Ok(response) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/server/src/remote/put.rs: -------------------------------------------------------------------------------- 1 | use crate::Server; 2 | use indoc::formatdoc; 3 | use tangram_client as tg; 4 | use tangram_database::{self as db, prelude::*}; 5 | use tangram_http::{Body, request::Ext as _, response::builder::Ext as _}; 6 | 7 | impl Server { 8 | pub async fn put_remote(&self, name: &str, arg: tg::remote::put::Arg) -> tg::Result<()> { 9 | let connection = self 10 | .database 11 | .write_connection() 12 | .await 13 | .map_err(|source| tg::error!(!source, "failed to get a database connection"))?; 14 | let p = connection.p(); 15 | let statement = formatdoc!( 16 | " 17 | insert into remotes (name, url) 18 | values ({p}1, {p}2) 19 | on conflict (name) 20 | do update set url = {p}2; 21 | ", 22 | ); 23 | let params = db::params![&name, &arg.url]; 24 | connection 25 | .execute(statement.into(), params) 26 | .await 27 | .map_err(|source| tg::error!(!source, "failed to insert the remote"))?; 28 | self.remotes.remove(name); 29 | Ok(()) 30 | } 31 | 32 | pub(crate) async fn handle_put_remote_request( 33 | handle: &H, 34 | request: http::Request, 35 | name: &str, 36 | ) -> tg::Result> 37 | where 38 | H: tg::Handle, 39 | { 40 | let arg = request.json().await?; 41 | handle.put_remote(name, arg).await?; 42 | let response = http::Response::builder().empty().unwrap(); 43 | Ok(response) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/server/src/runtime/builtin/checksum.rs: -------------------------------------------------------------------------------- 1 | use super::Runtime; 2 | use tangram_client as tg; 3 | 4 | impl Runtime { 5 | pub async fn checksum(&self, process: &tg::Process) -> tg::Result { 6 | let server = &self.server; 7 | let command = process.command(server).await?; 8 | 9 | // Get the args. 10 | let args = command.args(server).await?; 11 | 12 | // Get the object. 13 | let object = args 14 | .first() 15 | .ok_or_else(|| tg::error!("invalid number of arguments"))? 16 | .clone() 17 | .try_unwrap_object() 18 | .ok() 19 | .ok_or_else(|| tg::error!("expected an object"))?; 20 | 21 | // Get the algorithm. 22 | let algorithm = args 23 | .get(1) 24 | .ok_or_else(|| tg::error!("invalid number of arguments"))? 25 | .try_unwrap_string_ref() 26 | .ok() 27 | .ok_or_else(|| tg::error!("expected a string"))? 28 | .parse::() 29 | .map_err(|source| tg::error!(!source, "invalid algorithm"))?; 30 | 31 | // Compute the checksum. 32 | let checksum = if let Ok(blob) = tg::Blob::try_from(object.clone()) { 33 | self.server.checksum_blob(&blob, algorithm).await? 34 | } else if let Ok(artifact) = tg::Artifact::try_from(object.clone()) { 35 | self.server.checksum_artifact(&artifact, algorithm).await? 36 | } else { 37 | return Err(tg::error!("invalid object")); 38 | }; 39 | 40 | let output = checksum.to_string().into(); 41 | 42 | let output = crate::runtime::Output { 43 | checksum: None, 44 | error: None, 45 | exit: 0, 46 | output: Some(output), 47 | }; 48 | 49 | Ok(output) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/server/src/runtime/js/syscall/blob.rs: -------------------------------------------------------------------------------- 1 | use super::State; 2 | use bytes::Bytes; 3 | use std::{io::Cursor, rc::Rc}; 4 | use tangram_client as tg; 5 | use tangram_either::Either; 6 | use tokio::io::AsyncReadExt; 7 | 8 | pub async fn read( 9 | state: Rc, 10 | args: (tg::Blob, Option), 11 | ) -> tg::Result { 12 | let (blob, arg) = args; 13 | let arg = arg.unwrap_or_default(); 14 | let server = state.server.clone(); 15 | let bytes = state 16 | .main_runtime_handle 17 | .spawn(async move { 18 | let mut reader = blob.read(&server, arg).await?; 19 | let mut buffer = Vec::new(); 20 | reader 21 | .read_to_end(&mut buffer) 22 | .await 23 | .map_err(|source| tg::error!(!source, "failed to read the blob"))?; 24 | Ok::<_, tg::Error>(buffer.into()) 25 | }) 26 | .await 27 | .unwrap() 28 | .map_err(|source| tg::error!(!source, "failed to read the blob"))?; 29 | Ok(bytes) 30 | } 31 | 32 | pub async fn create(state: Rc, args: (Either,)) -> tg::Result { 33 | let (bytes,) = args; 34 | let reader = Cursor::new(bytes); 35 | let server = state.server.clone(); 36 | let blob = state 37 | .main_runtime_handle 38 | .spawn(async move { tg::Blob::with_reader(&server, reader).await }) 39 | .await 40 | .unwrap() 41 | .map_err(|source| tg::error!(!source, "failed to create the blob"))?; 42 | Ok(blob) 43 | } 44 | -------------------------------------------------------------------------------- /packages/server/src/runtime/js/syscall/checksum.rs: -------------------------------------------------------------------------------- 1 | use super::State; 2 | use bytes::Bytes; 3 | use std::rc::Rc; 4 | use tangram_client as tg; 5 | use tangram_either::Either; 6 | 7 | pub async fn checksum( 8 | _state: Rc, 9 | args: (Either, tg::checksum::Algorithm), 10 | ) -> tg::Result { 11 | let (bytes, algorithm) = args; 12 | let bytes = match &bytes { 13 | Either::Left(string) => string.as_bytes(), 14 | Either::Right(bytes) => bytes.as_ref(), 15 | }; 16 | let mut writer = tg::checksum::Writer::new(algorithm); 17 | writer.update(bytes); 18 | let checksum = writer.finalize(); 19 | Ok(checksum) 20 | } 21 | -------------------------------------------------------------------------------- /packages/server/src/runtime/js/syscall/log.rs: -------------------------------------------------------------------------------- 1 | use super::State; 2 | use std::rc::Rc; 3 | use tangram_client as tg; 4 | use tangram_v8::FromV8; 5 | 6 | pub struct Message { 7 | pub stream: Stream, 8 | pub string: String, 9 | } 10 | 11 | pub enum Stream { 12 | Stdout, 13 | Stderr, 14 | } 15 | 16 | pub fn log( 17 | _scope: &mut v8::HandleScope, 18 | state: Rc, 19 | args: (Stream, String), 20 | ) -> tg::Result<()> { 21 | let (stream, string) = args; 22 | let message = Message { stream, string }; 23 | if let Some(log_sender) = state.log_sender.borrow().as_ref() { 24 | log_sender.send(message).unwrap(); 25 | } 26 | Ok(()) 27 | } 28 | 29 | impl FromV8 for Stream { 30 | fn from_v8<'a>( 31 | scope: &mut v8::HandleScope<'a>, 32 | value: v8::Local<'a, v8::Value>, 33 | ) -> tg::Result { 34 | String::from_v8(scope, value)?.parse() 35 | } 36 | } 37 | 38 | impl std::fmt::Display for Stream { 39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 | match self { 41 | Self::Stdout => write!(f, "stdout"), 42 | Self::Stderr => write!(f, "stderr"), 43 | } 44 | } 45 | } 46 | 47 | impl std::str::FromStr for Stream { 48 | type Err = tg::Error; 49 | fn from_str(s: &str) -> Result { 50 | match s { 51 | "stdout" => Ok(Stream::Stdout), 52 | "stderr" => Ok(Stream::Stderr), 53 | stream => Err(tg::error!(%stream, "invalid stream")), 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/server/src/runtime/js/syscall/magic.rs: -------------------------------------------------------------------------------- 1 | use crate::runtime::js::State; 2 | use std::rc::Rc; 3 | use tangram_client as tg; 4 | use tangram_v8::ToV8 as _; 5 | 6 | pub fn magic<'s>( 7 | scope: &mut v8::HandleScope<'s>, 8 | args: &v8::FunctionCallbackArguments, 9 | ) -> tg::Result> { 10 | // Get the context. 11 | let context = scope.get_current_context(); 12 | 13 | // Get the state. 14 | let state = context.get_slot::>().unwrap().clone(); 15 | 16 | // Get the function. 17 | let arg = args.get(1); 18 | let function = v8::Local::::try_from(arg) 19 | .ok() 20 | .ok_or_else(|| tg::error!("expected a function"))?; 21 | 22 | // Get the module. 23 | let modules = state.modules.borrow(); 24 | let mut module = None; 25 | for module_ in modules.iter() { 26 | if let Some(v8_module) = &module_.v8 { 27 | let v8_module = v8::Local::new(scope, v8_module); 28 | if v8_module.script_id() == Some(function.get_script_origin().script_id()) { 29 | module = Some(tg::Module::from(module_.module.clone())); 30 | } 31 | } 32 | } 33 | let module = module.ok_or_else(|| tg::error!("failed to find the module for the function"))?; 34 | 35 | // Get the export. 36 | let export = Some(function.get_name(scope).to_rust_string_lossy(scope)); 37 | 38 | // Create the executable. 39 | let executable = 40 | tg::command::Executable::Module(tg::command::ModuleExecutable { module, export }); 41 | 42 | let value = executable.to_v8(scope)?; 43 | 44 | Ok(value) 45 | } 46 | -------------------------------------------------------------------------------- /packages/server/src/runtime/js/syscall/object.rs: -------------------------------------------------------------------------------- 1 | use super::State; 2 | use std::rc::Rc; 3 | use tangram_client as tg; 4 | 5 | pub async fn load(state: Rc, args: (tg::object::Id,)) -> tg::Result { 6 | let (id,) = args; 7 | let server = state.server.clone(); 8 | let object = state 9 | .main_runtime_handle 10 | .spawn({ 11 | let id = id.clone(); 12 | async move { tg::object::Handle::with_id(id).object(&server).await } 13 | }) 14 | .await 15 | .unwrap() 16 | .map_err(|source| tg::error!(!source, %id, "failed to load the object"))?; 17 | Ok(object) 18 | } 19 | 20 | pub async fn store(state: Rc, args: (tg::object::Object,)) -> tg::Result { 21 | let (object,) = args; 22 | let server = state.server.clone(); 23 | let id = state 24 | .main_runtime_handle 25 | .spawn(async move { tg::object::Handle::with_object(object).id(&server).await }) 26 | .await 27 | .unwrap() 28 | .map_err(|source| tg::error!(!source, "failed to store the object"))?; 29 | Ok(id) 30 | } 31 | -------------------------------------------------------------------------------- /packages/server/src/runtime/js/syscall/sleep.rs: -------------------------------------------------------------------------------- 1 | use super::State; 2 | use std::{rc::Rc, time::Duration}; 3 | use tangram_client as tg; 4 | 5 | pub async fn sleep(_state: Rc, args: (f64,)) -> tg::Result<()> { 6 | let (duration,) = args; 7 | let duration = Duration::from_secs_f64(duration); 8 | tokio::time::sleep(duration).await; 9 | Ok(()) 10 | } 11 | -------------------------------------------------------------------------------- /packages/server/src/tag.rs: -------------------------------------------------------------------------------- 1 | pub mod delete; 2 | pub mod get; 3 | pub mod list; 4 | pub mod pull; 5 | pub mod put; 6 | -------------------------------------------------------------------------------- /packages/server/src/tag/get.rs: -------------------------------------------------------------------------------- 1 | use crate::Server; 2 | use tangram_client as tg; 3 | use tangram_http::{Body, response::builder::Ext as _}; 4 | 5 | impl Server { 6 | pub async fn try_get_tag( 7 | &self, 8 | pattern: &tg::tag::Pattern, 9 | ) -> tg::Result> { 10 | let arg = tg::tag::list::Arg { 11 | length: Some(1), 12 | pattern: pattern.clone(), 13 | remote: None, 14 | reverse: true, 15 | }; 16 | let tg::tag::list::Output { data } = self.list_tags(arg).await?; 17 | let Some(output) = data.into_iter().next() else { 18 | return Ok(None); 19 | }; 20 | Ok(Some(output)) 21 | } 22 | 23 | pub(crate) async fn handle_get_tag_request( 24 | handle: &H, 25 | _request: http::Request, 26 | pattern: &[&str], 27 | ) -> tg::Result> 28 | where 29 | H: tg::Handle, 30 | { 31 | let pattern = pattern.join("/").parse()?; 32 | let Some(output) = handle.try_get_tag(&pattern).await? else { 33 | return Ok(http::Response::builder().not_found().empty().unwrap()); 34 | }; 35 | let response = http::Response::builder().json(output).unwrap(); 36 | Ok(response) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/server/src/tag/pull.rs: -------------------------------------------------------------------------------- 1 | use crate::Server; 2 | use futures::{TryStreamExt as _, stream::FuturesUnordered}; 3 | use tangram_client as tg; 4 | use tangram_either::Either; 5 | 6 | impl Server { 7 | pub(crate) async fn pull_tag( 8 | &self, 9 | pattern: tg::tag::Pattern, 10 | remote: Option, 11 | ) -> tg::Result<()> { 12 | let list = self 13 | .list_tags(tg::tag::list::Arg { 14 | length: None, 15 | remote: remote.clone(), 16 | pattern, 17 | reverse: false, 18 | }) 19 | .await? 20 | .data; 21 | list.into_iter() 22 | .filter_map(|output| { 23 | let directory = output.item.right()?.try_unwrap_directory().ok()?; 24 | let server = self.clone(); 25 | let remote = remote.clone().unwrap_or_else(|| "default".to_owned()); 26 | Some(async move { 27 | let arg = tg::pull::Arg { 28 | items: vec![Either::Right(directory.into())], 29 | remote: Some(remote), 30 | ..Default::default() 31 | }; 32 | let stream = server.pull(arg).await?; 33 | let mut stream = std::pin::pin!(stream); 34 | while stream.try_next().await?.is_some() {} 35 | Ok::<_, tg::Error>(()) 36 | }) 37 | }) 38 | .collect::>() 39 | .try_collect() 40 | .await 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/server/src/temp.rs: -------------------------------------------------------------------------------- 1 | use crate::{Server, util::fs::remove}; 2 | use std::path::{Path, PathBuf}; 3 | 4 | pub struct Temp { 5 | path: PathBuf, 6 | preserve: bool, 7 | server: Server, 8 | } 9 | 10 | impl Temp { 11 | pub fn new(server: &Server) -> Self { 12 | const ENCODING: data_encoding::Encoding = data_encoding_macro::new_encoding! { 13 | symbols: "0123456789abcdefghjkmnpqrstvwxyz", 14 | }; 15 | let id = uuid::Uuid::now_v7(); 16 | let id = ENCODING.encode(&id.into_bytes()); 17 | let path = server.temp_path().join(id); 18 | let preserve = server.config.advanced.preserve_temp_directories; 19 | let server = server.clone(); 20 | Self { 21 | path, 22 | preserve, 23 | server, 24 | } 25 | } 26 | 27 | pub fn path(&self) -> &Path { 28 | &self.path 29 | } 30 | } 31 | 32 | impl AsRef for Temp { 33 | fn as_ref(&self) -> &Path { 34 | self.path() 35 | } 36 | } 37 | 38 | impl Drop for Temp { 39 | fn drop(&mut self) { 40 | if !self.preserve { 41 | tokio::spawn({ 42 | let server = self.server.clone(); 43 | let path = self.path.clone(); 44 | async move { 45 | remove(&path).await.ok(); 46 | server.temp_paths.remove(&path); 47 | } 48 | }); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/server/src/util.rs: -------------------------------------------------------------------------------- 1 | pub mod fs; 2 | pub mod iter; 3 | pub mod path; 4 | -------------------------------------------------------------------------------- /packages/server/src/util/iter.rs: -------------------------------------------------------------------------------- 1 | pub trait Ext: Iterator { 2 | fn batches(self, size: usize) -> Batches 3 | where 4 | Self: Sized, 5 | { 6 | Batches::new(self, size) 7 | } 8 | } 9 | 10 | impl Ext for I where I: Iterator {} 11 | 12 | pub struct Batches 13 | where 14 | I: Iterator, 15 | { 16 | iter: I, 17 | size: usize, 18 | } 19 | 20 | impl Batches 21 | where 22 | I: Iterator, 23 | { 24 | fn new(iter: I, size: usize) -> Self { 25 | assert!(size > 0, "batch size must be greater than 0"); 26 | Batches { iter, size } 27 | } 28 | } 29 | 30 | impl Iterator for Batches 31 | where 32 | I: Iterator, 33 | { 34 | type Item = Vec; 35 | 36 | fn next(&mut self) -> Option { 37 | let mut batch = Vec::with_capacity(self.size); 38 | for _ in 0..self.size { 39 | match self.iter.next() { 40 | Some(item) => batch.push(item), 41 | None => { 42 | return if batch.is_empty() { None } else { Some(batch) }; 43 | }, 44 | } 45 | } 46 | Some(batch) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/server/src/util/path.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use tangram_client as tg; 3 | 4 | pub fn diff(src: &Path, dst: &Path) -> tg::Result { 5 | if !src.is_absolute() || !dst.is_absolute() { 6 | return Err(tg::error!("both paths must be absolute")); 7 | } 8 | let src_components: Vec<_> = src.components().collect(); 9 | let dst_components: Vec<_> = dst.components().collect(); 10 | let common_prefix_len = src_components 11 | .iter() 12 | .zip(dst_components.iter()) 13 | .take_while(|(a, b)| a == b) 14 | .count(); 15 | let parents_needed = src_components.len() - common_prefix_len; 16 | let mut result = PathBuf::new(); 17 | for _ in 0..parents_needed { 18 | result.push(".."); 19 | } 20 | for component in &dst_components[common_prefix_len..] { 21 | result.push(component); 22 | } 23 | Ok(result) 24 | } 25 | -------------------------------------------------------------------------------- /packages/temp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tangram_temp" 3 | 4 | description = { workspace = true } 5 | edition = { workspace = true } 6 | homepage = { workspace = true } 7 | license = { workspace = true } 8 | publish = false 9 | repository = { workspace = true } 10 | rust-version = { workspace = true } 11 | version = { workspace = true } 12 | 13 | [lints] 14 | workspace = true 15 | 16 | [dependencies] 17 | derive_more = { workspace = true } 18 | futures = { workspace = true } 19 | rand = { workspace = true } 20 | serde = { workspace = true } 21 | tangram_client = { workspace = true } 22 | tokio = { workspace = true } 23 | xattr = { workspace = true } 24 | -------------------------------------------------------------------------------- /packages/uri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tangram_uri" 3 | 4 | description = { workspace = true } 5 | edition = { workspace = true } 6 | homepage = { workspace = true } 7 | license = { workspace = true } 8 | publish = false 9 | repository = { workspace = true } 10 | rust-version = { workspace = true } 11 | version = { workspace = true } 12 | 13 | [lints] 14 | workspace = true 15 | 16 | [dependencies] 17 | derive_more = { workspace = true } 18 | once_cell = { workspace = true } 19 | regex = { workspace = true } 20 | serde_with = { workspace = true } 21 | -------------------------------------------------------------------------------- /packages/uri/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use self::reference::Reference; 2 | 3 | pub mod reference; 4 | -------------------------------------------------------------------------------- /packages/v8/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tangram_v8" 3 | 4 | description = { workspace = true } 5 | edition = { workspace = true } 6 | homepage = { workspace = true } 7 | license = { workspace = true } 8 | publish = false 9 | repository = { workspace = true } 10 | rust-version = { workspace = true } 11 | version = { workspace = true } 12 | 13 | [lints] 14 | workspace = true 15 | 16 | [dependencies] 17 | bytes = { workspace = true } 18 | derive_more = { workspace = true } 19 | num = { workspace = true } 20 | serde = { workspace = true } 21 | tangram_client = { workspace = true } 22 | tangram_either = { workspace = true } 23 | time = { workspace = true } 24 | v8 = { workspace = true } 25 | -------------------------------------------------------------------------------- /packages/v8/src/convert/builtin.rs: -------------------------------------------------------------------------------- 1 | use crate::{FromV8, ToV8}; 2 | use tangram_client as tg; 3 | 4 | impl ToV8 for tg::ArchiveFormat { 5 | fn to_v8<'a>(&self, scope: &mut v8::HandleScope<'a>) -> tg::Result> { 6 | self.to_string().to_v8(scope) 7 | } 8 | } 9 | 10 | impl FromV8 for tg::ArchiveFormat { 11 | fn from_v8<'a>( 12 | scope: &mut v8::HandleScope<'a>, 13 | value: v8::Local<'a, v8::Value>, 14 | ) -> tg::Result { 15 | String::from_v8(scope, value)?.parse() 16 | } 17 | } 18 | 19 | impl ToV8 for tg::CompressionFormat { 20 | fn to_v8<'a>(&self, scope: &mut v8::HandleScope<'a>) -> tg::Result> { 21 | self.to_string().to_v8(scope) 22 | } 23 | } 24 | 25 | impl FromV8 for tg::CompressionFormat { 26 | fn from_v8<'a>( 27 | scope: &mut v8::HandleScope<'a>, 28 | value: v8::Local<'a, v8::Value>, 29 | ) -> tg::Result { 30 | String::from_v8(scope, value)?.parse() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/v8/src/convert/checksum.rs: -------------------------------------------------------------------------------- 1 | use super::{FromV8, ToV8}; 2 | use tangram_client as tg; 3 | 4 | impl ToV8 for tg::Checksum { 5 | fn to_v8<'a>(&self, scope: &mut v8::HandleScope<'a>) -> tg::Result> { 6 | self.to_string().to_v8(scope) 7 | } 8 | } 9 | 10 | impl FromV8 for tg::Checksum { 11 | fn from_v8<'a>( 12 | scope: &mut v8::HandleScope<'a>, 13 | value: v8::Local<'a, v8::Value>, 14 | ) -> tg::Result { 15 | String::from_v8(scope, value)?.parse() 16 | } 17 | } 18 | 19 | impl ToV8 for tg::checksum::Algorithm { 20 | fn to_v8<'a>(&self, scope: &mut v8::HandleScope<'a>) -> tg::Result> { 21 | self.to_string().to_v8(scope) 22 | } 23 | } 24 | 25 | impl FromV8 for tg::checksum::Algorithm { 26 | fn from_v8<'a>( 27 | scope: &mut v8::HandleScope<'a>, 28 | value: v8::Local<'a, v8::Value>, 29 | ) -> tg::Result { 30 | String::from_v8(scope, value)?.parse() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/v8/src/convert/reference.rs: -------------------------------------------------------------------------------- 1 | use super::{FromV8, ToV8}; 2 | use tangram_client as tg; 3 | 4 | impl ToV8 for tg::Reference { 5 | fn to_v8<'a>(&self, scope: &mut v8::HandleScope<'a>) -> tg::Result> { 6 | self.to_string().to_v8(scope) 7 | } 8 | } 9 | 10 | impl FromV8 for tg::Reference { 11 | fn from_v8<'a>( 12 | scope: &mut v8::HandleScope<'a>, 13 | value: v8::Local<'a, v8::Value>, 14 | ) -> tg::Result { 15 | String::from_v8(scope, value)?.parse() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/v8/src/convert/serde.rs: -------------------------------------------------------------------------------- 1 | use super::{FromV8, ToV8, de::Deserializer, ser::Serializer}; 2 | use tangram_client as tg; 3 | 4 | pub struct Serde(T); 5 | 6 | #[derive(Debug, derive_more::Display, derive_more::Error, derive_more::From)] 7 | pub enum Error { 8 | Other(Box), 9 | } 10 | 11 | impl Serde { 12 | pub fn new(value: T) -> Self { 13 | Self(value) 14 | } 15 | 16 | pub fn into_inner(self) -> T { 17 | self.0 18 | } 19 | } 20 | 21 | impl ToV8 for Serde 22 | where 23 | T: serde::Serialize, 24 | { 25 | fn to_v8<'a>(&self, scope: &mut v8::HandleScope<'a>) -> tg::Result> { 26 | let serializer = Serializer::new(scope); 27 | let value = self 28 | .0 29 | .serialize(serializer) 30 | .map_err(|source| tg::error!(!source, "failed to convert the value to v8"))?; 31 | Ok(value) 32 | } 33 | } 34 | 35 | impl FromV8 for Serde 36 | where 37 | T: serde::de::DeserializeOwned, 38 | { 39 | fn from_v8<'a>( 40 | scope: &mut v8::HandleScope<'a>, 41 | value: v8::Local<'a, v8::Value>, 42 | ) -> tg::Result { 43 | let deserializer = Deserializer::new(scope, value); 44 | let value = Self( 45 | T::deserialize(deserializer) 46 | .map_err(|source| tg::error!(!source, "failed to convert the value from v8"))?, 47 | ); 48 | Ok(value) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/v8/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use self::convert::{FromV8, Serde, ToV8}; 2 | 3 | pub mod convert; 4 | -------------------------------------------------------------------------------- /packages/version/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tangram_version" 3 | 4 | description = { workspace = true } 5 | edition = { workspace = true } 6 | homepage = { workspace = true } 7 | license = { workspace = true } 8 | publish = false 9 | repository = { workspace = true } 10 | rust-version = { workspace = true } 11 | version = { workspace = true } 12 | 13 | [lints] 14 | workspace = true 15 | 16 | [dependencies] 17 | derive_more = { workspace = true } 18 | winnow = { workspace = true } 19 | -------------------------------------------------------------------------------- /packages/version/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use self::{pattern::Pattern, version::Version}; 2 | 3 | pub mod pattern; 4 | pub mod version; 5 | -------------------------------------------------------------------------------- /packages/vfs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tangram_vfs" 3 | 4 | description = { workspace = true } 5 | edition = { workspace = true } 6 | homepage = { workspace = true } 7 | license = { workspace = true } 8 | publish = false 9 | repository = { workspace = true } 10 | rust-version = { workspace = true } 11 | version = { workspace = true } 12 | 13 | [lints] 14 | workspace = true 15 | 16 | [dev-dependencies] 17 | clap = { workspace = true } 18 | tracing-subscriber = { workspace = true } 19 | 20 | [dependencies] 21 | async-channel = { workspace = true } 22 | bytes = { workspace = true } 23 | dashmap = { workspace = true } 24 | futures = { workspace = true } 25 | libc = { workspace = true } 26 | num = { workspace = true } 27 | tangram_either = { workspace = true } 28 | tangram_futures = { workspace = true } 29 | tokio = { workspace = true } 30 | tokio-util = { workspace = true } 31 | tracing = { workspace = true } 32 | zerocopy = { workspace = true } 33 | -------------------------------------------------------------------------------- /packages/vscode/.gitignore: -------------------------------------------------------------------------------- 1 | /*.vsix 2 | /extension.js 3 | /extension.js.map 4 | -------------------------------------------------------------------------------- /packages/vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | /.gitignore 2 | /.vscode/** 3 | /src/** 4 | /tsconfig.json 5 | -------------------------------------------------------------------------------- /packages/vscode/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Tangram, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/vscode/tangram-javascript-jsdoc-injection.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "injectionSelector": "L:comment.block.documentation", 3 | "patterns": [ 4 | { 5 | "include": "#jsdocbody" 6 | } 7 | ], 8 | "repository": { 9 | "jsdocbody": { 10 | "begin": "(?<=/\\*\\*)([^*]|\\*(?!/))*$", 11 | "while": "(^|\\G)\\s*\\*(?!/)(?=([^*]|[*](?!/))*$)", 12 | "patterns": [ 13 | { 14 | "include": "source.tg.js#docblock" 15 | } 16 | ] 17 | } 18 | }, 19 | "scopeName": "documentation.injection.tg.js" 20 | } 21 | -------------------------------------------------------------------------------- /packages/vscode/tangram-javascript.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "fileTypes": [".tg.js"], 4 | "name": "Tangram JavaScript", 5 | "patterns": [ 6 | { 7 | "include": "source.js" 8 | } 9 | ], 10 | "scopeName": "source.tg.js" 11 | } 12 | -------------------------------------------------------------------------------- /packages/vscode/tangram-typescript-jsdoc-injection.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "injectionSelector": "L:comment.block.documentation", 3 | "patterns": [ 4 | { 5 | "include": "#jsdocbody" 6 | } 7 | ], 8 | "repository": { 9 | "jsdocbody": { 10 | "begin": "(?<=/\\*\\*)([^*]|\\*(?!/))*$", 11 | "while": "(^|\\G)\\s*\\*(?!/)(?=([^*]|[*](?!/))*$)", 12 | "patterns": [ 13 | { 14 | "include": "source.tg.ts#docblock" 15 | } 16 | ] 17 | } 18 | }, 19 | "scopeName": "documentation.injection.tg.ts" 20 | } 21 | -------------------------------------------------------------------------------- /packages/vscode/tangram-typescript.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "fileTypes": [".tg.ts"], 4 | "name": "Tangram TypeScript", 5 | "patterns": [ 6 | { 7 | "include": "source.ts" 8 | } 9 | ], 10 | "scopeName": "source.tg.ts" 11 | } 12 | -------------------------------------------------------------------------------- /packages/vscode/tangram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangramdotdev/tangram/d5f1852fb8348b7c7fe6551120e099a08137de72/packages/vscode/tangram.png -------------------------------------------------------------------------------- /packages/vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "exactOptionalPropertyTypes": true, 4 | "lib": ["esnext"], 5 | "module": "commonjs", 6 | "noEmit": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true, 9 | "strict": true, 10 | "target": "esnext", 11 | "types": ["node", "vscode"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | match_block_trailing_comma = true 3 | use_field_init_shorthand = true 4 | -------------------------------------------------------------------------------- /tangram.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "kind": "directory", 5 | "entries": { 6 | "tangram.ts": 1 7 | }, 8 | "id": "dir_014r97ndvjbk8968xkb50ftvtc9kxwv5ng29enaahpq0bdx1v2xdmg" 9 | }, 10 | { 11 | "kind": "file", 12 | "id": "fil_01mx4qse40crmv7emn242096c4y13drc18n288qm0p6z9hb1we4se0" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /tangram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | --------------------------------------------------------------------------------