pnpm, verify, cleanup (#6)

This commit is contained in:
Hari 2026-03-31 23:59:52 -04:00 committed by GitHub
parent b68151035a
commit e2f03f47af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 4276 additions and 5133 deletions

15
.editorconfig Normal file
View 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
View file

@ -0,0 +1,7 @@
.git
.next
.turbo
coverage
dist
node_modules
apps/web/.next

View file

@ -30,8 +30,15 @@ Read these in order:
- `apps/node-agent`: Go NAS runtime / WebDAV node
- `apps/nextcloud-app`: optional Nextcloud adapter
- `packages/contracts`: canonical shared contracts
- `packages/sdk-ts`: TypeScript SDK surface for the web app
- `packages/ui`: shared React UI
- `infra/docker`: local Docker runtime
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
View 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.

View file

@ -60,12 +60,12 @@ func main() {
})
mux.HandleFunc("/api/v1/nodes/register", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, jsonObject{
"id": "dev-node",
"machineId": "dev-machine",
"displayName": "Development NAS",
"agentVersion": "0.1.0-dev",
"status": "online",
"lastSeenAt": time.Now().UTC().Format(time.RFC3339),
"id": "dev-node",
"machineId": "dev-machine",
"displayName": "Development NAS",
"agentVersion": "0.1.0-dev",
"status": "online",
"lastSeenAt": time.Now().UTC().Format(time.RFC3339),
"directAddress": env("BETTERNAS_NODE_DIRECT_ADDRESS", "http://localhost:8090"),
"relayAddress": nil,
})

View file

@ -5,6 +5,8 @@
"scripts": {
"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",
"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 ./...",
"check-types": "CGO_ENABLED=0 go test ./...",
"test": "CGO_ENABLED=0 go test ./..."

View file

@ -14,4 +14,3 @@
"nextcloud/ocp": "dev-stable31"
}
}

View file

@ -1,85 +1,84 @@
.betternas-shell {
max-width: 1100px;
margin: 0 auto;
padding: 32px;
max-width: 1100px;
margin: 0 auto;
padding: 32px;
}
.betternas-shell__hero {
margin-bottom: 28px;
padding: 28px;
border-radius: 24px;
background: linear-gradient(135deg, #10212d 0%, #184152 100%);
color: #f6fafc;
margin-bottom: 28px;
padding: 28px;
border-radius: 24px;
background: linear-gradient(135deg, #10212d 0%, #184152 100%);
color: #f6fafc;
}
.betternas-shell__eyebrow {
margin: 0 0 12px;
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
opacity: 0.8;
margin: 0 0 12px;
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
opacity: 0.8;
}
.betternas-shell__title {
margin: 0 0 12px;
font-size: 32px;
line-height: 1.1;
margin: 0 0 12px;
font-size: 32px;
line-height: 1.1;
}
.betternas-shell__copy {
margin: 0;
max-width: 70ch;
font-size: 15px;
line-height: 1.6;
margin: 0;
max-width: 70ch;
font-size: 15px;
line-height: 1.6;
}
.betternas-shell__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
.betternas-shell__card {
padding: 24px;
border: 1px solid rgba(16, 33, 45, 0.12);
border-radius: 20px;
background: #ffffff;
box-shadow: 0 20px 40px rgba(16, 33, 45, 0.06);
padding: 24px;
border: 1px solid rgba(16, 33, 45, 0.12);
border-radius: 20px;
background: #ffffff;
box-shadow: 0 20px 40px rgba(16, 33, 45, 0.06);
}
.betternas-shell__card h2 {
margin-top: 0;
margin-top: 0;
}
.betternas-shell__card dl {
display: grid;
grid-template-columns: minmax(120px, 160px) 1fr;
gap: 8px 16px;
margin: 0;
display: grid;
grid-template-columns: minmax(120px, 160px) 1fr;
gap: 8px 16px;
margin: 0;
}
.betternas-shell__card dt {
font-weight: 600;
font-weight: 600;
}
.betternas-shell__card dd {
margin: 0;
margin: 0;
}
.betternas-shell__card code {
display: inline-block;
padding: 4px 8px;
border-radius: 999px;
background: #eef4f7;
display: inline-block;
padding: 4px 8px;
border-radius: 999px;
background: #eef4f7;
}
.betternas-shell__card ul {
margin: 0;
padding-left: 20px;
margin: 0;
padding-left: 20px;
}
.betternas-shell__error {
margin-top: 16px;
color: #b42318;
margin-top: 16px;
color: #b42318;
}

View file

@ -5,6 +5,8 @@
"scripts": {
"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",
"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 ./...",
"check-types": "CGO_ENABLED=0 go test ./...",
"test": "CGO_ENABLED=0 go test ./..."

View file

@ -13,7 +13,8 @@ const geistMono = localFont({
export const metadata: Metadata = {
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({

View file

@ -2,7 +2,11 @@
min-height: 100svh;
padding: 48px 24px 80px;
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%);
color: #10212d;
}

View file

@ -25,7 +25,9 @@ export default function Home() {
<main className={styles.page}>
<section className={styles.hero}>
<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}>
The repo is organized so each system part can be built in parallel
without inventing new interfaces. The source of truth is the root
@ -35,7 +37,12 @@ export default function Home() {
<section className={styles.grid}>
{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}
</Card>
))}

View file

@ -11,15 +11,14 @@
"check-types": "next typegen && tsc --noEmit"
},
"dependencies": {
"@betternas/sdk-ts": "*",
"@betternas/ui": "*",
"@betternas/ui": "workspace:*",
"next": "16.2.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@betternas/eslint-config": "*",
"@betternas/typescript-config": "*",
"@betternas/eslint-config": "workspace:*",
"@betternas/typescript-config": "workspace:*",
"@types/node": "^22.15.3",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",

View file

@ -8,11 +8,6 @@
],
"strictNullChecks": true
},
"include": [
"**/*.ts",
"**/*.tsx",
"next-env.d.ts",
"next.config.js"
],
"include": ["**/*.ts", "**/*.tsx", "next-env.d.ts", "next.config.js"],
"exclude": ["node_modules"]
}

View file

@ -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.
It should run:
- a WebDAV server
- a small betterNAS node agent
- declarative config via Nix
- optional tunnel or relay connection if the machine is not directly reachable
It should expose one or more storage exports such as:
- `/data`
- `/media`
- `/backups`

View file

@ -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.
It should own:
- users
- devices
- NAS nodes

View file

@ -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.
It can start simple:
- Finder + WebDAV mount
- manual `Connect to Server`
It can later grow into:
- a small desktop helper
- one-click mount flows
- auto-mount at login

View file

@ -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.
This is where we can reuse Nextcloud heavily for:
- browser file UI
- uploads and downloads
- sharing links

View file

@ -3,6 +3,7 @@
This document ties the other four parts together.
It answers four questions:
- how the full system fits together
- where each part starts
- what we should steal from existing tools
@ -40,17 +41,18 @@ It answers four questions:
The control plane owns product semantics.
The other three parts are execution surfaces:
- the NAS node serves storage
- the local device mounts and uses storage
- the cloud/web layer exposes storage through browser and mobile-friendly flows
## What we steal vs write
| Part | Steal first | Write ourselves |
|---|---|---|
| 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 |
| Local device | Finder WebDAV mount, macOS Keychain, later maybe launch agent patterns | helper app, mount profile handling, auto-mount UX |
| Part | Steal first | Write ourselves |
| --------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| 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 |
| 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 |
## Where each part should start
@ -58,11 +60,13 @@ The other three parts are execution surfaces:
## 1. NAS node
Start from:
- Nix flake / module
- a standard WebDAV server
- a very small agent process
Do not start by writing:
- custom storage protocol
- custom file server
- custom sync engine
@ -72,6 +76,7 @@ The NAS node should be boring and reproducible.
## 2. Control plane
Start from:
- Go
- standard library routing first
- Postgres via `pgx` and `sqlc`
@ -80,6 +85,7 @@ Start from:
- standalone API mindset
Do not start by writing:
- microservices
- custom file transport
- 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
Start from:
- native Finder `Connect to Server`
- WebDAV mount URLs issued by the control plane
Then later add:
- a lightweight helper app
- Keychain integration
- auto-mount at login
Do not start by writing:
- a full custom desktop sync client
- a Finder extension
- a new filesystem driver
@ -105,16 +114,19 @@ Do not start by writing:
## 4. Cloud / web layer
Start from:
- stock Nextcloud
- current shell app
- Nextcloud browser/share/mobile primitives
Then later add:
- betterNAS-specific integration pages
- standalone control-plane web UI
- custom branding or replacement UI where justified
Do not start by writing:
- a full custom browser file manager
- a custom mobile client
- a custom sharing stack
@ -148,19 +160,23 @@ This is high leverage, but should not block Phase A.
## External parts we should deliberately reuse
### NAS node
- WebDAV server implementation
- Nix module patterns
### Control plane
- Go API service scaffold
- Postgres
- Redis
### Local device
- Finder's native WebDAV mounting
- macOS credential storage
### Cloud/web layer
- Nextcloud server
- Nextcloud app shell
- 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
### NAS node
- node enrollment
- export registration
- machine identity and health reporting
### Control plane
- full backend domain model
- access and policy model
- mount profile generation
@ -181,22 +199,24 @@ This is high leverage, but should not block Phase A.
- audit and registry
### Local device
- user-friendly mounting workflow
- helper app if needed
- local mount orchestration
### Cloud/web layer
- betterNAS-to-Nextcloud mapping layer
- standalone betterNAS product UI over time
## First scaffolds to use
| Part | First scaffold |
|---|---|
| NAS node | Nix flake/module + WebDAV server service config |
| Control plane | Go service + OpenAPI contract + Postgres/Redis adapters later |
| Local device | documented Finder mount flow, then lightweight helper app |
| Cloud/web layer | current Nextcloud scaffold and shell app |
| Part | First scaffold |
| --------------- | ------------------------------------------------------------- |
| NAS node | Nix flake/module + WebDAV server service config |
| Control plane | Go service + OpenAPI contract + Postgres/Redis adapters later |
| Local device | documented Finder mount flow, then lightweight helper app |
| Cloud/web layer | current Nextcloud scaffold and shell app |
## What not to overbuild early

View file

@ -123,12 +123,12 @@ parts exist at once.
Each area gets an owner and a narrow write surface.
| Part | Owns | May read | Must not own |
|---|---|---|---|
| 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 |
| 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 |
| Part | Owns | May read | Must not own |
| --------------- | ------------------------------------------------ | ----------------------------- | ------------------------------ |
| 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 |
| 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 |
The only shared write surface across teams should be:

View file

@ -38,7 +38,7 @@ services:
test:
[
"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
timeout: 5s

4928
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,10 +4,13 @@
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"guardrails": "node ./scripts/check-boundaries.mjs",
"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",
"test": "turbo run test"
"test": "turbo run test",
"verify": "pnpm run guardrails && pnpm run format:check && turbo run lint check-types test build"
},
"devDependencies": {
"prettier": "^3.7.4",
@ -17,9 +20,5 @@
"engines": {
"node": ">=18"
},
"packageManager": "npm@10.9.7",
"workspaces": [
"apps/*",
"packages/*"
]
"packageManager": "pnpm@10.33.0"
}

View file

@ -12,6 +12,6 @@
],
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc --noEmit -p tsconfig.json"
"check-types": "tsc --noEmit -p tsconfig.json"
}
}

View file

@ -3,13 +3,7 @@
"$id": "https://betternas.local/schemas/cloud-profile.schema.json",
"type": "object",
"additionalProperties": false,
"required": [
"id",
"exportId",
"provider",
"baseUrl",
"path"
],
"required": ["id", "exportId", "provider", "baseUrl", "path"],
"properties": {
"id": {
"type": "string"

View file

@ -3,13 +3,7 @@
"$id": "https://betternas.local/schemas/control-plane-health.schema.json",
"type": "object",
"additionalProperties": false,
"required": [
"service",
"status",
"timestamp",
"uptimeSeconds",
"nextcloud"
],
"required": ["service", "status", "timestamp", "uptimeSeconds", "nextcloud"],
"properties": {
"service": {
"const": "control-plane"
@ -26,11 +20,7 @@
"nextcloud": {
"type": "object",
"additionalProperties": false,
"required": [
"configured",
"baseUrl",
"provider"
],
"required": ["configured", "baseUrl", "provider"],
"properties": {
"configured": {
"type": "boolean"
@ -45,4 +35,3 @@
}
}
}

View file

@ -3,11 +3,7 @@
"$id": "https://betternas.local/schemas/control-plane-version.schema.json",
"type": "object",
"additionalProperties": false,
"required": [
"service",
"version",
"apiVersion"
],
"required": ["service", "version", "apiVersion"],
"properties": {
"service": {
"const": "control-plane"
@ -20,4 +16,3 @@
}
}
}

View file

@ -32,10 +32,7 @@
"type": "boolean"
},
"credentialMode": {
"enum": [
"session-token",
"app-password"
]
"enum": ["session-token", "app-password"]
}
}
}

View file

@ -27,26 +27,16 @@
"type": "string"
},
"status": {
"enum": [
"online",
"offline",
"degraded"
]
"enum": ["online", "offline", "degraded"]
},
"lastSeenAt": {
"type": "string"
},
"directAddress": {
"type": [
"string",
"null"
]
"type": ["string", "null"]
},
"relayAddress": {
"type": [
"string",
"null"
]
"type": ["string", "null"]
}
}
}

View file

@ -32,10 +32,7 @@
}
},
"capacityBytes": {
"type": [
"number",
"null"
]
"type": ["number", "null"]
},
"tags": {
"type": "array",

View file

@ -1,6 +1,6 @@
export const CONTROL_PLANE_ROUTES = {
health: "/health",
version: "/version"
version: "/version",
} as const;
export interface NextcloudBackendStatus {
@ -22,4 +22,3 @@ export interface ControlPlaneVersionResponse {
version: string;
apiVersion: "v1";
}

View file

@ -3,7 +3,7 @@ export const FOUNDATION_API_ROUTES = {
nodeHeartbeat: "/api/v1/nodes/:nodeId/heartbeat",
listExports: "/api/v1/exports",
issueMountProfile: "/api/v1/mount-profiles/issue",
issueCloudProfile: "/api/v1/cloud-profiles/issue"
issueCloudProfile: "/api/v1/cloud-profiles/issue",
} as const;
export type NasNodeStatus = "online" | "offline" | "degraded";

View file

@ -4,8 +4,5 @@
"rootDir": "src",
"outDir": "dist"
},
"include": [
"src/**/*.ts"
]
"include": ["src/**/*.ts"]
}

View file

@ -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.

View file

@ -1,3 +0,0 @@
import { config } from "@betternas/eslint-config/base";
export default config;

View file

@ -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"
}
}

View file

@ -1 +0,0 @@
export * from "@betternas/contracts";

View file

@ -1,8 +0,0 @@
{
"extends": "@betternas/typescript-config/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View file

@ -12,8 +12,8 @@
"check-types": "tsc --noEmit"
},
"devDependencies": {
"@betternas/eslint-config": "*",
"@betternas/typescript-config": "*",
"@betternas/eslint-config": "workspace:*",
"@betternas/typescript-config": "workspace:*",
"@types/node": "^22.15.3",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",

3967
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

3
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,3 @@
packages:
- "apps/*"
- "packages/*"

View 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) || ".";
}

View file

@ -21,7 +21,6 @@ betterNAS/
│ └── nextcloud-app/ # optional Nextcloud adapter
├── packages/
│ ├── contracts/ # canonical OpenAPI, schemas, TS types
│ ├── sdk-ts/ # TS SDK facade for the web app
│ ├── ui/ # shared React UI
│ ├── eslint-config/ # shared lint config
│ └── typescript-config/ # shared TS config
@ -36,14 +35,13 @@ betterNAS/
## Runtime and language choices
| Part | Language | Why |
|---|---|---|
| `apps/web` | TypeScript + Next.js | best UI velocity, best admin/control-plane UX |
| `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/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/sdk-ts` | TypeScript | ergonomic consumption from the Next.js app |
| Part | Language | Why |
| -------------------- | ---------------------------------- | -------------------------------------------------------------------- |
| `apps/web` | TypeScript + Next.js | best UI velocity, best admin/control-plane UX |
| `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/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 |
## Canonical contract rule
@ -89,7 +87,6 @@ Allowed ownership:
- Nextcloud mapping logic
- shared contract lane
- `packages/contracts`
- `packages/sdk-ts`
- `docs/architecture.md`
## 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 mapping between cloud mode and mount mode
- the repo contract and shared schemas
- the root `pnpm verify` loop
## The first implementation slices after this scaffold

View file

@ -11,4 +11,3 @@
"resolveJsonModule": true
}
}

View file

@ -10,6 +10,12 @@
"lint": {
"dependsOn": ["^lint"]
},
"format": {
"cache": false
},
"format:check": {
"dependsOn": ["^format:check"]
},
"check-types": {
"dependsOn": ["^check-types"]
},