mirror of
https://github.com/harivansh-afk/betterNAS.git
synced 2026-04-15 07:04:44 +00:00
pnpm, verify, cleanup (#6)
This commit is contained in:
parent
b68151035a
commit
e2f03f47af
45 changed files with 4276 additions and 5133 deletions
15
.editorconfig
Normal file
15
.editorconfig
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
7
.prettierignore
Normal file
7
.prettierignore
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
.git
|
||||||
|
.next
|
||||||
|
.turbo
|
||||||
|
coverage
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
apps/web/.next
|
||||||
|
|
@ -30,8 +30,15 @@ Read these in order:
|
||||||
- `apps/node-agent`: Go NAS runtime / WebDAV node
|
- `apps/node-agent`: Go NAS runtime / WebDAV node
|
||||||
- `apps/nextcloud-app`: optional Nextcloud adapter
|
- `apps/nextcloud-app`: optional Nextcloud adapter
|
||||||
- `packages/contracts`: canonical shared contracts
|
- `packages/contracts`: canonical shared contracts
|
||||||
- `packages/sdk-ts`: TypeScript SDK surface for the web app
|
|
||||||
- `packages/ui`: shared React UI
|
- `packages/ui`: shared React UI
|
||||||
- `infra/docker`: local Docker runtime
|
- `infra/docker`: local Docker runtime
|
||||||
|
|
||||||
The root planning and delegation guide lives in [skeleton.md](/home/rathi/Documents/GitHub/betterNAS/skeleton.md).
|
The root planning and delegation guide lives in [skeleton.md](/home/rathi/Documents/GitHub/betterNAS/skeleton.md).
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
Run the repo acceptance loop with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm verify
|
||||||
|
```
|
||||||
|
|
|
||||||
7
TODO.md
Normal file
7
TODO.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# TODO
|
||||||
|
|
||||||
|
- [x] Remove the temporary TypeScript SDK layer so shared interfaces only come from `packages/contracts`.
|
||||||
|
- [x] Switch the monorepo from `npm` workspaces to `pnpm`.
|
||||||
|
- [x] Add root formatting, verification, and Go formatting rails.
|
||||||
|
- [x] Add hard boundary checks so apps and packages cannot drift across lanes with private imports.
|
||||||
|
- [ ] Make the first contract-backed mount loop real: node registration, export inventory, mount profile issuance, and a Finder-mountable WebDAV export.
|
||||||
|
|
@ -60,12 +60,12 @@ func main() {
|
||||||
})
|
})
|
||||||
mux.HandleFunc("/api/v1/nodes/register", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/v1/nodes/register", func(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSON(w, http.StatusOK, jsonObject{
|
writeJSON(w, http.StatusOK, jsonObject{
|
||||||
"id": "dev-node",
|
"id": "dev-node",
|
||||||
"machineId": "dev-machine",
|
"machineId": "dev-machine",
|
||||||
"displayName": "Development NAS",
|
"displayName": "Development NAS",
|
||||||
"agentVersion": "0.1.0-dev",
|
"agentVersion": "0.1.0-dev",
|
||||||
"status": "online",
|
"status": "online",
|
||||||
"lastSeenAt": time.Now().UTC().Format(time.RFC3339),
|
"lastSeenAt": time.Now().UTC().Format(time.RFC3339),
|
||||||
"directAddress": env("BETTERNAS_NODE_DIRECT_ADDRESS", "http://localhost:8090"),
|
"directAddress": env("BETTERNAS_NODE_DIRECT_ADDRESS", "http://localhost:8090"),
|
||||||
"relayAddress": nil,
|
"relayAddress": nil,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "CGO_ENABLED=0 go run ./cmd/control-plane",
|
"dev": "CGO_ENABLED=0 go run ./cmd/control-plane",
|
||||||
"build": "mkdir -p dist && CGO_ENABLED=0 go build -o dist/control-plane ./cmd/control-plane",
|
"build": "mkdir -p dist && CGO_ENABLED=0 go build -o dist/control-plane ./cmd/control-plane",
|
||||||
|
"format": "files=$(find . -name '*.go' -type f) && if [ -n \"$files\" ]; then gofmt -w $files; fi",
|
||||||
|
"format:check": "files=$(find . -name '*.go' -type f) && if [ -n \"$files\" ]; then test -z \"$(gofmt -l $files)\"; fi",
|
||||||
"lint": "CGO_ENABLED=0 go vet ./...",
|
"lint": "CGO_ENABLED=0 go vet ./...",
|
||||||
"check-types": "CGO_ENABLED=0 go test ./...",
|
"check-types": "CGO_ENABLED=0 go test ./...",
|
||||||
"test": "CGO_ENABLED=0 go test ./..."
|
"test": "CGO_ENABLED=0 go test ./..."
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,3 @@
|
||||||
"nextcloud/ocp": "dev-stable31"
|
"nextcloud/ocp": "dev-stable31"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,85 +1,84 @@
|
||||||
.betternas-shell {
|
.betternas-shell {
|
||||||
max-width: 1100px;
|
max-width: 1100px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.betternas-shell__hero {
|
.betternas-shell__hero {
|
||||||
margin-bottom: 28px;
|
margin-bottom: 28px;
|
||||||
padding: 28px;
|
padding: 28px;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
background: linear-gradient(135deg, #10212d 0%, #184152 100%);
|
background: linear-gradient(135deg, #10212d 0%, #184152 100%);
|
||||||
color: #f6fafc;
|
color: #f6fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.betternas-shell__eyebrow {
|
.betternas-shell__eyebrow {
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.12em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.betternas-shell__title {
|
.betternas-shell__title {
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.betternas-shell__copy {
|
.betternas-shell__copy {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
max-width: 70ch;
|
max-width: 70ch;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.betternas-shell__grid {
|
.betternas-shell__grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.betternas-shell__card {
|
.betternas-shell__card {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border: 1px solid rgba(16, 33, 45, 0.12);
|
border: 1px solid rgba(16, 33, 45, 0.12);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
box-shadow: 0 20px 40px rgba(16, 33, 45, 0.06);
|
box-shadow: 0 20px 40px rgba(16, 33, 45, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.betternas-shell__card h2 {
|
.betternas-shell__card h2 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.betternas-shell__card dl {
|
.betternas-shell__card dl {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(120px, 160px) 1fr;
|
grid-template-columns: minmax(120px, 160px) 1fr;
|
||||||
gap: 8px 16px;
|
gap: 8px 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.betternas-shell__card dt {
|
.betternas-shell__card dt {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.betternas-shell__card dd {
|
.betternas-shell__card dd {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.betternas-shell__card code {
|
.betternas-shell__card code {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: #eef4f7;
|
background: #eef4f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.betternas-shell__card ul {
|
.betternas-shell__card ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.betternas-shell__error {
|
.betternas-shell__error {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
color: #b42318;
|
color: #b42318;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "CGO_ENABLED=0 go run ./cmd/node-agent",
|
"dev": "CGO_ENABLED=0 go run ./cmd/node-agent",
|
||||||
"build": "mkdir -p dist && CGO_ENABLED=0 go build -o dist/node-agent ./cmd/node-agent",
|
"build": "mkdir -p dist && CGO_ENABLED=0 go build -o dist/node-agent ./cmd/node-agent",
|
||||||
|
"format": "files=$(find . -name '*.go' -type f) && if [ -n \"$files\" ]; then gofmt -w $files; fi",
|
||||||
|
"format:check": "files=$(find . -name '*.go' -type f) && if [ -n \"$files\" ]; then test -z \"$(gofmt -l $files)\"; fi",
|
||||||
"lint": "CGO_ENABLED=0 go vet ./...",
|
"lint": "CGO_ENABLED=0 go vet ./...",
|
||||||
"check-types": "CGO_ENABLED=0 go test ./...",
|
"check-types": "CGO_ENABLED=0 go test ./...",
|
||||||
"test": "CGO_ENABLED=0 go test ./..."
|
"test": "CGO_ENABLED=0 go test ./..."
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ const geistMono = localFont({
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "betterNAS",
|
title: "betterNAS",
|
||||||
description: "Contract-first monorepo for NAS mounts and optional cloud access",
|
description:
|
||||||
|
"Contract-first monorepo for NAS mounts and optional cloud access",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@
|
||||||
min-height: 100svh;
|
min-height: 100svh;
|
||||||
padding: 48px 24px 80px;
|
padding: 48px 24px 80px;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top left, rgba(91, 186, 166, 0.18), transparent 28%),
|
radial-gradient(
|
||||||
|
circle at top left,
|
||||||
|
rgba(91, 186, 166, 0.18),
|
||||||
|
transparent 28%
|
||||||
|
),
|
||||||
linear-gradient(180deg, #f5fbfa 0%, #edf5f3 100%);
|
linear-gradient(180deg, #f5fbfa 0%, #edf5f3 100%);
|
||||||
color: #10212d;
|
color: #10212d;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,9 @@ export default function Home() {
|
||||||
<main className={styles.page}>
|
<main className={styles.page}>
|
||||||
<section className={styles.hero}>
|
<section className={styles.hero}>
|
||||||
<p className={styles.eyebrow}>betterNAS monorepo</p>
|
<p className={styles.eyebrow}>betterNAS monorepo</p>
|
||||||
<h1 className={styles.title}>Contract-first scaffold for NAS mounts and cloud mode.</h1>
|
<h1 className={styles.title}>
|
||||||
|
Contract-first scaffold for NAS mounts and cloud mode.
|
||||||
|
</h1>
|
||||||
<p className={styles.copy}>
|
<p className={styles.copy}>
|
||||||
The repo is organized so each system part can be built in parallel
|
The repo is organized so each system part can be built in parallel
|
||||||
without inventing new interfaces. The source of truth is the root
|
without inventing new interfaces. The source of truth is the root
|
||||||
|
|
@ -35,7 +37,12 @@ export default function Home() {
|
||||||
|
|
||||||
<section className={styles.grid}>
|
<section className={styles.grid}>
|
||||||
{lanes.map((lane) => (
|
{lanes.map((lane) => (
|
||||||
<Card key={lane.title} className={styles.card} title={lane.title} href="/#">
|
<Card
|
||||||
|
key={lane.title}
|
||||||
|
className={styles.card}
|
||||||
|
title={lane.title}
|
||||||
|
href="/#"
|
||||||
|
>
|
||||||
{lane.body}
|
{lane.body}
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,14 @@
|
||||||
"check-types": "next typegen && tsc --noEmit"
|
"check-types": "next typegen && tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@betternas/sdk-ts": "*",
|
"@betternas/ui": "workspace:*",
|
||||||
"@betternas/ui": "*",
|
|
||||||
"next": "16.2.0",
|
"next": "16.2.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0"
|
"react-dom": "^19.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@betternas/eslint-config": "*",
|
"@betternas/eslint-config": "workspace:*",
|
||||||
"@betternas/typescript-config": "*",
|
"@betternas/typescript-config": "workspace:*",
|
||||||
"@types/node": "^22.15.3",
|
"@types/node": "^22.15.3",
|
||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.2",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,6 @@
|
||||||
],
|
],
|
||||||
"strictNullChecks": true
|
"strictNullChecks": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["**/*.ts", "**/*.tsx", "next-env.d.ts", "next.config.js"],
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx",
|
|
||||||
"next-env.d.ts",
|
|
||||||
"next.config.js"
|
|
||||||
],
|
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,14 @@ This document describes the software that runs on the actual NAS machine, VM, or
|
||||||
The NAS node is the machine that actually has the storage.
|
The NAS node is the machine that actually has the storage.
|
||||||
|
|
||||||
It should run:
|
It should run:
|
||||||
|
|
||||||
- a WebDAV server
|
- a WebDAV server
|
||||||
- a small betterNAS node agent
|
- a small betterNAS node agent
|
||||||
- declarative config via Nix
|
- declarative config via Nix
|
||||||
- optional tunnel or relay connection if the machine is not directly reachable
|
- optional tunnel or relay connection if the machine is not directly reachable
|
||||||
|
|
||||||
It should expose one or more storage exports such as:
|
It should expose one or more storage exports such as:
|
||||||
|
|
||||||
- `/data`
|
- `/data`
|
||||||
- `/media`
|
- `/media`
|
||||||
- `/backups`
|
- `/backups`
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ This document describes the main backend that owns product semantics and coordin
|
||||||
The control plane is the source of truth for betterNAS.
|
The control plane is the source of truth for betterNAS.
|
||||||
|
|
||||||
It should own:
|
It should own:
|
||||||
|
|
||||||
- users
|
- users
|
||||||
- devices
|
- devices
|
||||||
- NAS nodes
|
- NAS nodes
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,12 @@ This document describes the software and user experience on the user's Mac or ot
|
||||||
The local device layer is how a user actually mounts and uses their NAS.
|
The local device layer is how a user actually mounts and uses their NAS.
|
||||||
|
|
||||||
It can start simple:
|
It can start simple:
|
||||||
|
|
||||||
- Finder + WebDAV mount
|
- Finder + WebDAV mount
|
||||||
- manual `Connect to Server`
|
- manual `Connect to Server`
|
||||||
|
|
||||||
It can later grow into:
|
It can later grow into:
|
||||||
|
|
||||||
- a small desktop helper
|
- a small desktop helper
|
||||||
- one-click mount flows
|
- one-click mount flows
|
||||||
- auto-mount at login
|
- auto-mount at login
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ This document describes the optional browser, mobile, and cloud-drive style acce
|
||||||
The cloud/web layer is the part of betterNAS that makes storage accessible beyond local mounts.
|
The cloud/web layer is the part of betterNAS that makes storage accessible beyond local mounts.
|
||||||
|
|
||||||
This is where we can reuse Nextcloud heavily for:
|
This is where we can reuse Nextcloud heavily for:
|
||||||
|
|
||||||
- browser file UI
|
- browser file UI
|
||||||
- uploads and downloads
|
- uploads and downloads
|
||||||
- sharing links
|
- sharing links
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
This document ties the other four parts together.
|
This document ties the other four parts together.
|
||||||
|
|
||||||
It answers four questions:
|
It answers four questions:
|
||||||
|
|
||||||
- how the full system fits together
|
- how the full system fits together
|
||||||
- where each part starts
|
- where each part starts
|
||||||
- what we should steal from existing tools
|
- what we should steal from existing tools
|
||||||
|
|
@ -40,17 +41,18 @@ It answers four questions:
|
||||||
The control plane owns product semantics.
|
The control plane owns product semantics.
|
||||||
|
|
||||||
The other three parts are execution surfaces:
|
The other three parts are execution surfaces:
|
||||||
|
|
||||||
- the NAS node serves storage
|
- the NAS node serves storage
|
||||||
- the local device mounts and uses storage
|
- the local device mounts and uses storage
|
||||||
- the cloud/web layer exposes storage through browser and mobile-friendly flows
|
- the cloud/web layer exposes storage through browser and mobile-friendly flows
|
||||||
|
|
||||||
## What we steal vs write
|
## What we steal vs write
|
||||||
|
|
||||||
| Part | Steal first | Write ourselves |
|
| Part | Steal first | Write ourselves |
|
||||||
|---|---|---|
|
| --------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||||
| NAS node | NixOS/Nix module patterns, existing WebDAV servers | node agent, export model, node registration flow |
|
| NAS node | NixOS/Nix module patterns, existing WebDAV servers | node agent, export model, node registration flow |
|
||||||
| Control plane | Go stdlib routing, pgx/sqlc, go-redis/asynq, OpenAPI codegen | product domain model, policy engine, mount/cloud APIs, registry |
|
| Control plane | Go stdlib routing, pgx/sqlc, go-redis/asynq, OpenAPI codegen | product domain model, policy engine, mount/cloud APIs, registry |
|
||||||
| Local device | Finder WebDAV mount, macOS Keychain, later maybe launch agent patterns | helper app, mount profile handling, auto-mount UX |
|
| Local device | Finder WebDAV mount, macOS Keychain, later maybe launch agent patterns | helper app, mount profile handling, auto-mount UX |
|
||||||
| Cloud/web layer | Nextcloud server, Nextcloud shell app, Nextcloud share/file UI, Nextcloud mobile references | betterNAS integration layer, mapping between product model and Nextcloud, later branded UI |
|
| Cloud/web layer | Nextcloud server, Nextcloud shell app, Nextcloud share/file UI, Nextcloud mobile references | betterNAS integration layer, mapping between product model and Nextcloud, later branded UI |
|
||||||
|
|
||||||
## Where each part should start
|
## Where each part should start
|
||||||
|
|
@ -58,11 +60,13 @@ The other three parts are execution surfaces:
|
||||||
## 1. NAS node
|
## 1. NAS node
|
||||||
|
|
||||||
Start from:
|
Start from:
|
||||||
|
|
||||||
- Nix flake / module
|
- Nix flake / module
|
||||||
- a standard WebDAV server
|
- a standard WebDAV server
|
||||||
- a very small agent process
|
- a very small agent process
|
||||||
|
|
||||||
Do not start by writing:
|
Do not start by writing:
|
||||||
|
|
||||||
- custom storage protocol
|
- custom storage protocol
|
||||||
- custom file server
|
- custom file server
|
||||||
- custom sync engine
|
- custom sync engine
|
||||||
|
|
@ -72,6 +76,7 @@ The NAS node should be boring and reproducible.
|
||||||
## 2. Control plane
|
## 2. Control plane
|
||||||
|
|
||||||
Start from:
|
Start from:
|
||||||
|
|
||||||
- Go
|
- Go
|
||||||
- standard library routing first
|
- standard library routing first
|
||||||
- Postgres via `pgx` and `sqlc`
|
- Postgres via `pgx` and `sqlc`
|
||||||
|
|
@ -80,6 +85,7 @@ Start from:
|
||||||
- standalone API mindset
|
- standalone API mindset
|
||||||
|
|
||||||
Do not start by writing:
|
Do not start by writing:
|
||||||
|
|
||||||
- microservices
|
- microservices
|
||||||
- custom file transport
|
- custom file transport
|
||||||
- a proxy that sits in the middle of every file transfer
|
- a proxy that sits in the middle of every file transfer
|
||||||
|
|
@ -89,15 +95,18 @@ This is the first real thing we should build.
|
||||||
## 3. Local device
|
## 3. Local device
|
||||||
|
|
||||||
Start from:
|
Start from:
|
||||||
|
|
||||||
- native Finder `Connect to Server`
|
- native Finder `Connect to Server`
|
||||||
- WebDAV mount URLs issued by the control plane
|
- WebDAV mount URLs issued by the control plane
|
||||||
|
|
||||||
Then later add:
|
Then later add:
|
||||||
|
|
||||||
- a lightweight helper app
|
- a lightweight helper app
|
||||||
- Keychain integration
|
- Keychain integration
|
||||||
- auto-mount at login
|
- auto-mount at login
|
||||||
|
|
||||||
Do not start by writing:
|
Do not start by writing:
|
||||||
|
|
||||||
- a full custom desktop sync client
|
- a full custom desktop sync client
|
||||||
- a Finder extension
|
- a Finder extension
|
||||||
- a new filesystem driver
|
- a new filesystem driver
|
||||||
|
|
@ -105,16 +114,19 @@ Do not start by writing:
|
||||||
## 4. Cloud / web layer
|
## 4. Cloud / web layer
|
||||||
|
|
||||||
Start from:
|
Start from:
|
||||||
|
|
||||||
- stock Nextcloud
|
- stock Nextcloud
|
||||||
- current shell app
|
- current shell app
|
||||||
- Nextcloud browser/share/mobile primitives
|
- Nextcloud browser/share/mobile primitives
|
||||||
|
|
||||||
Then later add:
|
Then later add:
|
||||||
|
|
||||||
- betterNAS-specific integration pages
|
- betterNAS-specific integration pages
|
||||||
- standalone control-plane web UI
|
- standalone control-plane web UI
|
||||||
- custom branding or replacement UI where justified
|
- custom branding or replacement UI where justified
|
||||||
|
|
||||||
Do not start by writing:
|
Do not start by writing:
|
||||||
|
|
||||||
- a full custom browser file manager
|
- a full custom browser file manager
|
||||||
- a custom mobile client
|
- a custom mobile client
|
||||||
- a custom sharing stack
|
- a custom sharing stack
|
||||||
|
|
@ -148,19 +160,23 @@ This is high leverage, but should not block Phase A.
|
||||||
## External parts we should deliberately reuse
|
## External parts we should deliberately reuse
|
||||||
|
|
||||||
### NAS node
|
### NAS node
|
||||||
|
|
||||||
- WebDAV server implementation
|
- WebDAV server implementation
|
||||||
- Nix module patterns
|
- Nix module patterns
|
||||||
|
|
||||||
### Control plane
|
### Control plane
|
||||||
|
|
||||||
- Go API service scaffold
|
- Go API service scaffold
|
||||||
- Postgres
|
- Postgres
|
||||||
- Redis
|
- Redis
|
||||||
|
|
||||||
### Local device
|
### Local device
|
||||||
|
|
||||||
- Finder's native WebDAV mounting
|
- Finder's native WebDAV mounting
|
||||||
- macOS credential storage
|
- macOS credential storage
|
||||||
|
|
||||||
### Cloud/web layer
|
### Cloud/web layer
|
||||||
|
|
||||||
- Nextcloud server
|
- Nextcloud server
|
||||||
- Nextcloud app shell
|
- Nextcloud app shell
|
||||||
- Nextcloud share/browser behavior
|
- Nextcloud share/browser behavior
|
||||||
|
|
@ -169,11 +185,13 @@ This is high leverage, but should not block Phase A.
|
||||||
## From-scratch parts we should deliberately own
|
## From-scratch parts we should deliberately own
|
||||||
|
|
||||||
### NAS node
|
### NAS node
|
||||||
|
|
||||||
- node enrollment
|
- node enrollment
|
||||||
- export registration
|
- export registration
|
||||||
- machine identity and health reporting
|
- machine identity and health reporting
|
||||||
|
|
||||||
### Control plane
|
### Control plane
|
||||||
|
|
||||||
- full backend domain model
|
- full backend domain model
|
||||||
- access and policy model
|
- access and policy model
|
||||||
- mount profile generation
|
- mount profile generation
|
||||||
|
|
@ -181,22 +199,24 @@ This is high leverage, but should not block Phase A.
|
||||||
- audit and registry
|
- audit and registry
|
||||||
|
|
||||||
### Local device
|
### Local device
|
||||||
|
|
||||||
- user-friendly mounting workflow
|
- user-friendly mounting workflow
|
||||||
- helper app if needed
|
- helper app if needed
|
||||||
- local mount orchestration
|
- local mount orchestration
|
||||||
|
|
||||||
### Cloud/web layer
|
### Cloud/web layer
|
||||||
|
|
||||||
- betterNAS-to-Nextcloud mapping layer
|
- betterNAS-to-Nextcloud mapping layer
|
||||||
- standalone betterNAS product UI over time
|
- standalone betterNAS product UI over time
|
||||||
|
|
||||||
## First scaffolds to use
|
## First scaffolds to use
|
||||||
|
|
||||||
| Part | First scaffold |
|
| Part | First scaffold |
|
||||||
|---|---|
|
| --------------- | ------------------------------------------------------------- |
|
||||||
| NAS node | Nix flake/module + WebDAV server service config |
|
| NAS node | Nix flake/module + WebDAV server service config |
|
||||||
| Control plane | Go service + OpenAPI contract + Postgres/Redis adapters later |
|
| Control plane | Go service + OpenAPI contract + Postgres/Redis adapters later |
|
||||||
| Local device | documented Finder mount flow, then lightweight helper app |
|
| Local device | documented Finder mount flow, then lightweight helper app |
|
||||||
| Cloud/web layer | current Nextcloud scaffold and shell app |
|
| Cloud/web layer | current Nextcloud scaffold and shell app |
|
||||||
|
|
||||||
## What not to overbuild early
|
## What not to overbuild early
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,12 +123,12 @@ parts exist at once.
|
||||||
|
|
||||||
Each area gets an owner and a narrow write surface.
|
Each area gets an owner and a narrow write surface.
|
||||||
|
|
||||||
| Part | Owns | May read | Must not own |
|
| Part | Owns | May read | Must not own |
|
||||||
|---|---|---|---|
|
| --------------- | ------------------------------------------------ | ----------------------------- | ------------------------------ |
|
||||||
| NAS node | node runtime, export reporting, WebDAV config | contracts, control-plane docs | product policy |
|
| NAS node | node runtime, export reporting, WebDAV config | contracts, control-plane docs | product policy |
|
||||||
| Control plane | domain model, grants, profile issuance, registry | everything | direct file serving by default |
|
| Control plane | domain model, grants, profile issuance, registry | everything | direct file serving by default |
|
||||||
| Local device | mount UX, helper flows, credential handling | contracts, control-plane docs | access policy |
|
| Local device | mount UX, helper flows, credential handling | contracts, control-plane docs | access policy |
|
||||||
| Cloud/web layer | Nextcloud adapter, browser/mobile integration | contracts, control-plane docs | source of truth |
|
| Cloud/web layer | Nextcloud adapter, browser/mobile integration | contracts, control-plane docs | source of truth |
|
||||||
|
|
||||||
The only shared write surface across teams should be:
|
The only shared write surface across teams should be:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ services:
|
||||||
test:
|
test:
|
||||||
[
|
[
|
||||||
"CMD-SHELL",
|
"CMD-SHELL",
|
||||||
"node -e \"fetch('http://127.0.0.1:3000/health').then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))\""
|
'node -e "fetch(''http://127.0.0.1:3000/health'').then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))"',
|
||||||
]
|
]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
|
|
|
||||||
4928
package-lock.json
generated
4928
package-lock.json
generated
File diff suppressed because it is too large
Load diff
13
package.json
13
package.json
|
|
@ -4,10 +4,13 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"dev": "turbo run dev",
|
"dev": "turbo run dev",
|
||||||
|
"guardrails": "node ./scripts/check-boundaries.mjs",
|
||||||
"lint": "turbo run lint",
|
"lint": "turbo run lint",
|
||||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
"format": "prettier --ignore-unknown --write . && turbo run format",
|
||||||
|
"format:check": "prettier --ignore-unknown --check . && turbo run format:check",
|
||||||
"check-types": "turbo run check-types",
|
"check-types": "turbo run check-types",
|
||||||
"test": "turbo run test"
|
"test": "turbo run test",
|
||||||
|
"verify": "pnpm run guardrails && pnpm run format:check && turbo run lint check-types test build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
|
|
@ -17,9 +20,5 @@
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"packageManager": "npm@10.9.7",
|
"packageManager": "pnpm@10.33.0"
|
||||||
"workspaces": [
|
|
||||||
"apps/*",
|
|
||||||
"packages/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,6 @@
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
"check-types": "tsc --noEmit -p tsconfig.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,7 @@
|
||||||
"$id": "https://betternas.local/schemas/cloud-profile.schema.json",
|
"$id": "https://betternas.local/schemas/cloud-profile.schema.json",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": ["id", "exportId", "provider", "baseUrl", "path"],
|
||||||
"id",
|
|
||||||
"exportId",
|
|
||||||
"provider",
|
|
||||||
"baseUrl",
|
|
||||||
"path"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,7 @@
|
||||||
"$id": "https://betternas.local/schemas/control-plane-health.schema.json",
|
"$id": "https://betternas.local/schemas/control-plane-health.schema.json",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": ["service", "status", "timestamp", "uptimeSeconds", "nextcloud"],
|
||||||
"service",
|
|
||||||
"status",
|
|
||||||
"timestamp",
|
|
||||||
"uptimeSeconds",
|
|
||||||
"nextcloud"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"service": {
|
"service": {
|
||||||
"const": "control-plane"
|
"const": "control-plane"
|
||||||
|
|
@ -26,11 +20,7 @@
|
||||||
"nextcloud": {
|
"nextcloud": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": ["configured", "baseUrl", "provider"],
|
||||||
"configured",
|
|
||||||
"baseUrl",
|
|
||||||
"provider"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"configured": {
|
"configured": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
|
@ -45,4 +35,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,7 @@
|
||||||
"$id": "https://betternas.local/schemas/control-plane-version.schema.json",
|
"$id": "https://betternas.local/schemas/control-plane-version.schema.json",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": ["service", "version", "apiVersion"],
|
||||||
"service",
|
|
||||||
"version",
|
|
||||||
"apiVersion"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"service": {
|
"service": {
|
||||||
"const": "control-plane"
|
"const": "control-plane"
|
||||||
|
|
@ -20,4 +16,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,7 @@
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"credentialMode": {
|
"credentialMode": {
|
||||||
"enum": [
|
"enum": ["session-token", "app-password"]
|
||||||
"session-token",
|
|
||||||
"app-password"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,26 +27,16 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"enum": [
|
"enum": ["online", "offline", "degraded"]
|
||||||
"online",
|
|
||||||
"offline",
|
|
||||||
"degraded"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"lastSeenAt": {
|
"lastSeenAt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"directAddress": {
|
"directAddress": {
|
||||||
"type": [
|
"type": ["string", "null"]
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"relayAddress": {
|
"relayAddress": {
|
||||||
"type": [
|
"type": ["string", "null"]
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"capacityBytes": {
|
"capacityBytes": {
|
||||||
"type": [
|
"type": ["number", "null"]
|
||||||
"number",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"tags": {
|
"tags": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export const CONTROL_PLANE_ROUTES = {
|
export const CONTROL_PLANE_ROUTES = {
|
||||||
health: "/health",
|
health: "/health",
|
||||||
version: "/version"
|
version: "/version",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export interface NextcloudBackendStatus {
|
export interface NextcloudBackendStatus {
|
||||||
|
|
@ -22,4 +22,3 @@ export interface ControlPlaneVersionResponse {
|
||||||
version: string;
|
version: string;
|
||||||
apiVersion: "v1";
|
apiVersion: "v1";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ export const FOUNDATION_API_ROUTES = {
|
||||||
nodeHeartbeat: "/api/v1/nodes/:nodeId/heartbeat",
|
nodeHeartbeat: "/api/v1/nodes/:nodeId/heartbeat",
|
||||||
listExports: "/api/v1/exports",
|
listExports: "/api/v1/exports",
|
||||||
issueMountProfile: "/api/v1/mount-profiles/issue",
|
issueMountProfile: "/api/v1/mount-profiles/issue",
|
||||||
issueCloudProfile: "/api/v1/cloud-profiles/issue"
|
issueCloudProfile: "/api/v1/cloud-profiles/issue",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type NasNodeStatus = "online" | "offline" | "degraded";
|
export type NasNodeStatus = "online" | "offline" | "degraded";
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,5 @@
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"outDir": "dist"
|
"outDir": "dist"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*.ts"]
|
||||||
"src/**/*.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
# `@betternas/sdk-ts`
|
|
||||||
|
|
||||||
Temporary TypeScript-facing SDK surface for the Next.js app.
|
|
||||||
|
|
||||||
The source of truth remains:
|
|
||||||
|
|
||||||
- [`packages/contracts/openapi`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/openapi)
|
|
||||||
- [`packages/contracts/src`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/src)
|
|
||||||
|
|
||||||
Later this package should become generated code from the OpenAPI contracts.
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import { config } from "@betternas/eslint-config/base";
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@betternas/sdk-ts",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"exports": {
|
|
||||||
".": "./src/index.ts"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc -p tsconfig.json",
|
|
||||||
"lint": "eslint . --max-warnings 0",
|
|
||||||
"check-types": "tsc --noEmit"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@betternas/contracts": "*"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@betternas/eslint-config": "*",
|
|
||||||
"@betternas/typescript-config": "*",
|
|
||||||
"@types/node": "^22.15.3",
|
|
||||||
"eslint": "^9.39.1",
|
|
||||||
"typescript": "5.9.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "@betternas/contracts";
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "@betternas/typescript-config/base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.ts"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
|
||||||
|
|
@ -12,8 +12,8 @@
|
||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@betternas/eslint-config": "*",
|
"@betternas/eslint-config": "workspace:*",
|
||||||
"@betternas/typescript-config": "*",
|
"@betternas/typescript-config": "workspace:*",
|
||||||
"@types/node": "^22.15.3",
|
"@types/node": "^22.15.3",
|
||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.2",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
|
|
|
||||||
3967
pnpm-lock.yaml
generated
Normal file
3967
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
packages:
|
||||||
|
- "apps/*"
|
||||||
|
- "packages/*"
|
||||||
117
scripts/check-boundaries.mjs
Normal file
117
scripts/check-boundaries.mjs
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const repoRoot = process.cwd();
|
||||||
|
const sourceExtensions = new Set([".js", ".mjs", ".cjs", ".ts", ".tsx"]);
|
||||||
|
const ignoredDirectories = new Set([
|
||||||
|
".git",
|
||||||
|
".next",
|
||||||
|
".turbo",
|
||||||
|
"coverage",
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const laneRoots = [];
|
||||||
|
for (const baseDir of ["apps", "packages"]) {
|
||||||
|
const absoluteBaseDir = path.join(repoRoot, baseDir);
|
||||||
|
for (const entry of readdirSync(absoluteBaseDir, { withFileTypes: true })) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
laneRoots.push(path.join(absoluteBaseDir, entry.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const disallowedWorkspacePackages = new Set([
|
||||||
|
"@betternas/sdk-ts",
|
||||||
|
"@betternas/web",
|
||||||
|
"@betternas/control-plane",
|
||||||
|
"@betternas/node-agent",
|
||||||
|
"@betternas/nextcloud-app",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const importPattern =
|
||||||
|
/\b(?:import|export)\b[\s\S]*?\bfrom\s*["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)/g;
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
walk(path.join(repoRoot, "apps"));
|
||||||
|
walk(path.join(repoRoot, "packages"));
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
console.error("Boundary check failed:\n");
|
||||||
|
for (const error of errors) {
|
||||||
|
console.error(`- ${error}`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Boundary check passed.");
|
||||||
|
|
||||||
|
function walk(currentPath) {
|
||||||
|
const stat = statSync(currentPath);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
if (ignoredDirectories.has(path.basename(currentPath))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of readdirSync(currentPath, { withFileTypes: true })) {
|
||||||
|
walk(path.join(currentPath, entry.name));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sourceExtensions.has(path.extname(currentPath))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContent = readFileSync(currentPath, "utf8");
|
||||||
|
const fileRoot = findLaneRoot(currentPath);
|
||||||
|
if (fileRoot === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const match of fileContent.matchAll(importPattern)) {
|
||||||
|
const specifier = match[1] ?? match[2];
|
||||||
|
if (!specifier) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disallowedWorkspacePackages.has(specifier)) {
|
||||||
|
errors.push(
|
||||||
|
`${relativeToRepo(currentPath)} imports forbidden workspace package ${specifier}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!specifier.startsWith(".")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedImport = path.resolve(path.dirname(currentPath), specifier);
|
||||||
|
const targetRoot = findLaneRoot(resolvedImport);
|
||||||
|
if (targetRoot !== null && targetRoot !== fileRoot) {
|
||||||
|
errors.push(
|
||||||
|
`${relativeToRepo(currentPath)} crosses lane boundary with relative import ${specifier}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLaneRoot(targetPath) {
|
||||||
|
const normalizedTargetPath = path.normalize(targetPath);
|
||||||
|
for (const laneRoot of laneRoots) {
|
||||||
|
const normalizedLaneRoot = path.normalize(laneRoot);
|
||||||
|
if (
|
||||||
|
normalizedTargetPath === normalizedLaneRoot ||
|
||||||
|
normalizedTargetPath.startsWith(`${normalizedLaneRoot}${path.sep}`)
|
||||||
|
) {
|
||||||
|
return normalizedLaneRoot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function relativeToRepo(targetPath) {
|
||||||
|
return path.relative(repoRoot, targetPath) || ".";
|
||||||
|
}
|
||||||
18
skeleton.md
18
skeleton.md
|
|
@ -21,7 +21,6 @@ betterNAS/
|
||||||
│ └── nextcloud-app/ # optional Nextcloud adapter
|
│ └── nextcloud-app/ # optional Nextcloud adapter
|
||||||
├── packages/
|
├── packages/
|
||||||
│ ├── contracts/ # canonical OpenAPI, schemas, TS types
|
│ ├── contracts/ # canonical OpenAPI, schemas, TS types
|
||||||
│ ├── sdk-ts/ # TS SDK facade for the web app
|
|
||||||
│ ├── ui/ # shared React UI
|
│ ├── ui/ # shared React UI
|
||||||
│ ├── eslint-config/ # shared lint config
|
│ ├── eslint-config/ # shared lint config
|
||||||
│ └── typescript-config/ # shared TS config
|
│ └── typescript-config/ # shared TS config
|
||||||
|
|
@ -36,14 +35,13 @@ betterNAS/
|
||||||
|
|
||||||
## Runtime and language choices
|
## Runtime and language choices
|
||||||
|
|
||||||
| Part | Language | Why |
|
| Part | Language | Why |
|
||||||
|---|---|---|
|
| -------------------- | ---------------------------------- | -------------------------------------------------------------------- |
|
||||||
| `apps/web` | TypeScript + Next.js | best UI velocity, best admin/control-plane UX |
|
| `apps/web` | TypeScript + Next.js | best UI velocity, best admin/control-plane UX |
|
||||||
| `apps/control-plane` | Go | strong concurrency, static binaries, operationally simple |
|
| `apps/control-plane` | Go | strong concurrency, static binaries, operationally simple |
|
||||||
| `apps/node-agent` | Go | best fit for host runtime, WebDAV service, and future Nix deployment |
|
| `apps/node-agent` | Go | best fit for host runtime, WebDAV service, and future Nix deployment |
|
||||||
| `apps/nextcloud-app` | PHP | native language for the Nextcloud adapter surface |
|
| `apps/nextcloud-app` | PHP | native language for the Nextcloud adapter surface |
|
||||||
| `packages/contracts` | OpenAPI + JSON Schema + TypeScript | language-neutral source of truth with practical TS ergonomics |
|
| `packages/contracts` | OpenAPI + JSON Schema + TypeScript | language-neutral source of truth with practical TS ergonomics |
|
||||||
| `packages/sdk-ts` | TypeScript | ergonomic consumption from the Next.js app |
|
|
||||||
|
|
||||||
## Canonical contract rule
|
## Canonical contract rule
|
||||||
|
|
||||||
|
|
@ -89,7 +87,6 @@ Allowed ownership:
|
||||||
- Nextcloud mapping logic
|
- Nextcloud mapping logic
|
||||||
- shared contract lane
|
- shared contract lane
|
||||||
- `packages/contracts`
|
- `packages/contracts`
|
||||||
- `packages/sdk-ts`
|
|
||||||
- `docs/architecture.md`
|
- `docs/architecture.md`
|
||||||
|
|
||||||
## The first verification loop
|
## The first verification loop
|
||||||
|
|
@ -253,6 +250,7 @@ If a task does not make one of those steps more real, it is probably too early.
|
||||||
- the mount profile model
|
- the mount profile model
|
||||||
- the mapping between cloud mode and mount mode
|
- the mapping between cloud mode and mount mode
|
||||||
- the repo contract and shared schemas
|
- the repo contract and shared schemas
|
||||||
|
- the root `pnpm verify` loop
|
||||||
|
|
||||||
## The first implementation slices after this scaffold
|
## The first implementation slices after this scaffold
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,3 @@
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,12 @@
|
||||||
"lint": {
|
"lint": {
|
||||||
"dependsOn": ["^lint"]
|
"dependsOn": ["^lint"]
|
||||||
},
|
},
|
||||||
|
"format": {
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
|
"format:check": {
|
||||||
|
"dependsOn": ["^format:check"]
|
||||||
|
},
|
||||||
"check-types": {
|
"check-types": {
|
||||||
"dependsOn": ["^check-types"]
|
"dependsOn": ["^check-types"]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue