Design decisions
Rows marked PROVISIONAL can change before v1. Rows marked Decided are settled.
| Question | Answer | Status | Why |
|---|---|---|---|
Free-flag sigil |
|
Decided |
Free flags are unordered and mean the same thing in every command, and |
Manifest format |
JSON5 ( |
Decided |
Manifests are written by people, so comments, trailing commas and unquoted keys help. JSON5 parses to the JSON data model, so the existing JSON Schemas (draft 2020-12) still validate it, and plain JSON is still valid input. The D library uses a small in-repo JSON5-to-JSON translator ( |
Namespace tree storage |
SQLite index compiled from the JSON5 manifests |
PROVISIONAL |
|
Command name for the resolver |
|
Decided |
Ryan’s rationale: "I wanted the command to literally be 'install' not 'osh-install' otherwise it breaks the illusion of a seamless unified interface." In the namespace, |
install(1) passthrough |
Hand install(1)-style arguments to |
PROVISIONAL |
Keeps |
Implementation language |
D (LDC), packaged with dub |
PROVISIONAL |
The Open Shell runtime and shell-context stubs are in D. It builds native single binaries for Windows, Linux and macOS. |
Legacy subtree name |
|
PROVISIONAL |
The name says what the subtree is, and it sorts away from real verbs. |
Inspection tool name |
|
Decided |
|
Namespace tree storage (PROVISIONAL)
The tree has two layers:
-
Source of truth: the JSON5 registration manifests, one per package. People and installers write these.
-
Compiled index: a store built from the manifests that answers resolve, children and conflict queries quickly. It can always be rebuilt, so it is a cache, not data.
Options compared
| Need | YAML file(s) | SQLite index | JSON5 file tree (one file per node or package) |
|---|---|---|---|
Concurrent installer writes |
Poor. One shared file means read-modify-write races, and it needs an external lock file. |
Good. Transactions with WAL; several installers can register at once, and a conflicting claim fails atomically. |
Fair. One file per package avoids most races, but a check-then-write across files still needs a lock. |
Conflict-query speed |
Poor. Parse everything, then scan. |
Good. An index on |
Fair. Walk and parse the tree, or keep a separate index anyway. |
Human inspectability |
Good. |
Fair. Needs |
Good. |
Diffability |
Good, but one big file gives noisy diffs. |
Poor as a binary file. Mitigated: diff the manifests, or dump the index as text. |
Good, with small focused diffs. |
Fits the JSON5 source decision |
Adds a second text format. |
Yes. It is a compiled view of the manifests. |
Yes, but then it is also the index, so it still has the query problem. |
Recommendation
SQLite as the compiled index, with JSON5 manifests as the source of truth (PROVISIONAL). This keeps the readable, diffable parts (the manifests) readable and diffable, and gives the tool safe concurrent writes and fast conflict checks where they matter. If the index is lost or corrupted, a rebuild from the manifests restores it.
Rules (proposed):
-
Installers write their manifest, then register it in one transaction. A refused conflict rolls back the whole package.
-
nameand the shell read from the index.name rebuild(future) recompiles it from the manifests. -
The index stores a hash of each manifest. If a manifest changes on disk, the index is out of date and gets rebuilt.
Proposed tables
CREATE TABLE package (
id TEXT NOT NULL,
version TEXT NOT NULL,
manifest_path TEXT NOT NULL,
manifest_hash TEXT NOT NULL, -- sha256 of the JSON5 source
policy TEXT NOT NULL CHECK (policy IN ('refuse','shadow-with-warning')),
registered_at TEXT NOT NULL,
PRIMARY KEY (id, version)
);
CREATE TABLE claim (
rowid INTEGER PRIMARY KEY, -- registration order (shadowing)
path TEXT NOT NULL, -- node path, NS-1
parent TEXT NOT NULL, -- '' for top level; children queries
kind TEXT NOT NULL CHECK (kind IN ('verb','provider','command','alias','group')),
summary TEXT,
argv_json TEXT, -- entry argv as a JSON array
style TEXT NOT NULL DEFAULT 'strict',
target TEXT, -- alias target
source TEXT NOT NULL, -- 'manifest:<id>@<ver>' or 'legacy:PATH:<dir>'
pkg_id TEXT,
pkg_version TEXT,
FOREIGN KEY (pkg_id, pkg_version) REFERENCES package(id, version) ON DELETE CASCADE
);
CREATE INDEX claim_path ON claim(path);
CREATE INDEX claim_parent ON claim(parent);
CREATE TABLE signature (
node TEXT NOT NULL,
pkg_id TEXT NOT NULL,
body_json TEXT NOT NULL -- input signature, validated JSON
);
-- conflicts (NS-3)
-- SELECT path, count(*) FROM claim GROUP BY path HAVING count(*) > 1;
Not implemented in v0. The in-memory Registry is the reference behaviour that a store must match.
Coexisting with install(1)
On Unix PATH-based shells, coreutils/BSD install(1) already exists and copies files and sets modes.
-
Namespace model:
installis the rootinstallnode.install(1)is registered atlegacy/install.installresolves to the resolver;legacy/installreaches the file copier. -
Plain PATH shells: packaging MUST NOT overwrite
/usr/bin/install. Put the resolver ahead on the user’s PATH only (for example~/.local/bin), or use a shell alias or function. Never change the system PATH. -
Build scripts: scripts that call
install(1)with its file-copy flags (-m,-D,-d,-o,-g, …) MUST keep working. -
Passthrough (PROVISIONAL, implemented in openshellorg/install): when the arguments look like install(1) (any install(1) option, or an existing source file followed by a destination), the resolver hands them unchanged to the real install(1) at a fixed system path (
/usr/bin/install,/bin/install, or Homebrewginstall), never found by searching PATH, and returns its exit status. Resolver commands and flags (search,managers,help,--help,--version) always win, and bareinstallprints help. -
Windows: there is no
installcommand, so there is no clash.