Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 33 additions & 13 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,17 @@ Platform configs win over Dockerfiles win over inferred package.json scripts. Us
- Explicit services present → explicit wins. Derived services are subsumed: their data becomes `DirContext` and is layered onto all explicit services. (Compose defines "api" + "worker", plain Dockerfile exists → Compose wins, both get the dockerfile path via context.)
- Explicit services with the same name at the same dir → merged (same signal registration order applies).

**Step 3.2 — Merge DirContext per directory.** Multiple signals may emit context for the same directory (e.g., `DotEnv` and `Package` both describe `apps/api/`). Merge all by directoryfirst non-None wins per field, env vars collected from all.
**Step 3.2 — Group DirContexts per directory by language.** Multiple signals may emit context for the same directory (e.g., `DotEnv` and `Package` both describe `apps/api/`). Contexts are grouped by directory, then by language within each directory. Same-language contexts merge (first non-None wins per field). Different-language contexts stay separate — this is how multi-language directories work. A dir with Ruby (from Gemfile) and JavaScript (from package.json) keeps both as separate context groups. Contexts without a language (e.g., from platform signals like Railway or Vercel) are wildcards: they merge into the first existing context group, filling gaps.

**Step 3.3 — Layer context with ancestor inheritance.** For each service, build a chain from root to service dir. Root context is the base; each child overrides. Then layer the merged chain onto the service — service's own fields win, None fields filled from context. Env vars merged: service's keys take priority.
**Step 3.3 — Layer context with ancestor inheritance.** For each service, build an ancestor chain from root to service dir (one context per language group). Child overrides parent per language group. Convert each context group to a `Runtime` and set on the service. Runtimes are sorted: non-JS/TS backends first, JS/TS last.

This propagates a root `.node-version` or root `.env` down to all services while letting subdirectory files override them.

**Step 3.4 — Promote unclaimed directories.** A `DirContext` with a `start` command whose directory isn't already claimed by a service becomes a service. Named from directory. Also handles HTML/SPA contexts without start commands. This is the most common real-world case: a plain repo with `package.json` and a start script.
**Step 3.4 — Promote unclaimed directories.** A `DirContext` with a `start` command whose directory isn't already claimed by a service becomes a service. Each language group becomes a `Runtime` on the promoted service. Named from directory. Also handles HTML/SPA contexts without start commands. This is the most common real-world case: a plain repo with `package.json` and a start script.

**Step 3.5 — Enrich with monorepo.** If a `Monorepo` was detected, match services to packages by directory and annotate `detected_by`.

The key invariant: `Service::layer_context()` and `DirContext::merge()` both use "self wins" semantics — the closer/earlier source always takes priority over a more distant/later one.
The key invariant: `Service::layer_contexts()` and `DirContext::merge()` both use "self wins" semantics — the closer/earlier source always takes priority over a more distant/later one.

## FileSystem Abstraction

Expand Down Expand Up @@ -195,7 +195,7 @@ This gives parallel I/O for large repos while keeping signal observation single-
| `src/lib.rs` | Public API: `discover_local()`, `discover_with_fs()` |
| `src/main.rs` | CLI: `launch [PATH] [--format json\|json-pretty]` |
| `src/error.rs` | `LaunchError`: Filesystem, Parse, Config variants |
| `src/types.rs` | All output types: `Discovery`, `Service`, `DirContext`, `Commands`, `EnvVar`, `Monorepo`, enums; `merge_env_vars()`, `Service::layer_context()` |
| `src/types.rs` | All output types: `Discovery`, `Service`, `Runtime`, `DirContext`, `Commands`, `EnvVar`, `Monorepo`, enums; `merge_env_vars()`, `Service::layer_contexts()` |
| `src/signal.rs` | `Signal` trait, `SignalOutput`, `read_config()` helper |
| `src/fs.rs` | `FileSystem` trait, `LocalFs`, `MemoryFs`, `DirEntry` |
| `src/discovery.rs` | Walk, generate, assemble — the entire pipeline |
Expand Down Expand Up @@ -243,17 +243,11 @@ pub struct Discovery {
pub struct Service {
pub name: String, // "api"
pub dir: String, // "apps/api"
pub language: Option<Language>,
pub language_config: Option<LanguageConfig>, // language-specific details (NodeConfig, PythonConfig, ...)
pub runtime: Option<RuntimeInfo>, // { name: "node", version: "20.11.1", source: ".nvmrc" }
pub framework: Option<String>,
pub package_manager: Option<PackageManagerInfo>,
pub runtimes: Vec<Runtime>, // one per language (Ruby + JS = 2 entries)
pub network: Option<Network>, // Private | Public
pub exec_mode: Option<ExecMode>, // Daemon | Scheduled
pub commands: Commands, // install, build, start, dev
pub image: Option<String>, // pre-built container image (from docker-compose)
pub dockerfile: Option<String>,
pub output_dir: Option<String>, // for SPAs and static sites
pub env: Vec<EnvVar>,
pub system_deps: Vec<String>,
pub volumes: Vec<Volume>,
Expand All @@ -265,13 +259,39 @@ pub struct Service {
pub detected_by: Vec<String>, // provenance
}

// One language's full context within a service
pub struct Runtime {
pub language: Language,
pub name: String, // "node", "ruby", "python"
pub version: Option<String>,
pub version_source: Option<String>,
pub package_manager: Option<PackageManagerInfo>,
pub framework: Option<String>,
pub language_config: Option<LanguageConfig>,
pub output_dir: Option<String>,
pub install: Option<String>,
pub build: Option<String>,
pub start: Option<String>,
pub dev: Option<String>,
}

// Directory-level context (signals don't know service count)
pub struct DirContext {
pub dir: String,
// same fields as Service minus identity/deployment fields
pub language: Option<Language>,
pub runtime: Option<RuntimeInfo>,
pub framework: Option<String>,
pub package_manager: Option<PackageManagerInfo>,
pub language_config: Option<LanguageConfig>,
pub output_dir: Option<String>,
pub commands: Commands, // install, build, start, dev
pub env: Vec<EnvVar>,
pub system_deps: Vec<String>,
}
```

`Service` has convenience methods (`language()`, `framework()`, `start()`, etc.) that delegate to the first matching runtime. `runtimes` is sorted: non-JS/TS backends first, JS/TS last — so `runtimes[0]` is always the primary language.

## Design Decisions

### Why service signals vs context signals?
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "paraglide-launch"
version = "0.1.2"
version = "0.2.0"
edition = "2024"
description = "Analyze a project and detect deployable services, languages, frameworks, commands, and env vars"
license = "MIT"
Expand Down
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ $ launch ./my-app
{
"name": "api",
"dir": "apps/api",
"language": "typescript",
"runtime": { "name": "node", "version": "20.11.1", "source": ".nvmrc" },
"framework": "nextjs",
"package_manager": { "name": "pnpm" },
"commands": {
"install": "pnpm install",
"build": "next build",
"start": "next start",
"dev": "next dev"
},
"runtimes": [
{
"language": "typescript",
"name": "node",
"version": "20.11.1",
"version_source": ".nvmrc",
"package_manager": { "name": "pnpm" },
"framework": "next",
"install": "pnpm install",
"build": "next build",
"start": "next start",
"dev": "next dev"
}
],
"env": [
{ "key": "DATABASE_URL" },
{ "key": "PORT", "default": "3000" }
Expand All @@ -29,6 +33,8 @@ $ launch ./my-app
}
```

Multi-language directories (Rails + React, Django + React, Laravel + Vue) produce a single service with multiple runtimes — the backend runtime first, the JS/TS runtime second.

## Install

```
Expand Down
Loading