@dhzh/foundry 0.0.3-beta.1 → 0.0.3-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,9 +5,8 @@ An AI-native local developer runtime for tools, skills, agents, and workflows.
5
5
  Foundry is distributed as a CLI package. It starts a local Hono server and
6
6
  serves the bundled React Web UI from `dist/web/`.
7
7
 
8
- The Web UI is built with Astryx and currently provides a minimal React 19
9
- starter surface. Additional dashboard, settings, and tools experiences are
10
- described in the implementation plans under [`specs/plans/`](specs/plans/).
8
+ The Web UI is built with Astryx and provides the local Foundry administration
9
+ shell, including the Settings surface.
11
10
 
12
11
  ## Requirements
13
12
 
@@ -50,10 +49,7 @@ pnpm run build:cli # Build only dist/cli/
50
49
  pnpm run build:web # Build only dist/web/
51
50
  pnpm run dev:cli # Run the CLI from source in watch mode
52
51
  pnpm run dev:web # Start the Rsbuild Web UI development server
53
- pnpm run preview:web # Preview the production Web UI build
54
52
  pnpm run lint # Run ESLint
55
- pnpm run test # Run Vitest once
56
- pnpm run test:coverage # Run Vitest with coverage
57
53
  ```
58
54
 
59
55
  ## Run A Production Build
@@ -74,6 +70,24 @@ pnpm dlx @dhzh/foundry
74
70
  The `foundry` executable listens on `127.0.0.1:7777` and prints a clickable
75
71
  local URL when it starts.
76
72
 
73
+ ## Settings
74
+
75
+ The Settings capability is available through the Web UI and the module-level
76
+ CLI command:
77
+
78
+ Current registered settings are `ui.theme` and `ui.pointer`.
79
+
80
+ ```sh
81
+ foundry settings list
82
+ foundry settings get ui.theme
83
+ foundry settings set ui.theme dark
84
+ foundry settings reset ui.theme
85
+ ```
86
+
87
+ Use `--raw` when only command values or boolean mutation results are needed.
88
+ The installable [`foundry-settings`](skills/foundry-settings/SKILL.md) Skill is
89
+ the automation wrapper for this CLI surface.
90
+
77
91
  ## Web UI
78
92
 
79
93
  The private [`web`](packages/web/package.json) workspace uses:
@@ -99,7 +113,10 @@ the CLI can serve them next to its bundled output.
99
113
 
100
114
  | Path | Role |
101
115
  |------|------|
116
+ | `specs/plans/` | Numbered implementation plans and their naming convention |
102
117
  | `src/cli/` | CLI entrypoint and Hono static server |
118
+ | `src/modules/settings/` | Settings registry, repository, service, routes, and CLI command |
119
+ | `skills/foundry-settings/` | Installable Skill wrapping the Settings CLI command |
103
120
  | `packages/web/` | Rsbuild + React Web UI workspace |
104
121
  | `packages/web/src/index.tsx` | Browser entrypoint |
105
122
  | `packages/web/src/app.tsx` | Root React component |
@@ -108,7 +125,11 @@ the CLI can serve them next to its bundled output.
108
125
  | `bin/index.js` | Published `foundry` executable wrapper |
109
126
  | `dist/cli/` | Bundled CLI output generated by tsdown |
110
127
  | `dist/web/` | Web UI production output served by the CLI |
111
- | `specs/plans/` | Numbered implementation plans and design history |
128
+
129
+ ## Implementation Plans
130
+
131
+ See [`specs/plans/README.md`](specs/plans/README.md) for the canonical plan
132
+ location, naming, numbering, and content conventions.
112
133
 
113
134
  ## Learn More
114
135
 
@@ -1 +1,25 @@
1
- import e from"cac";import t from"terminal-link";import{existsSync as n}from"node:fs";import r from"node:path";import{exit as i}from"node:process";import{Hono as a}from"hono";import{serve as o}from"@hono/node-server";import{serveStatic as s}from"@hono/node-server/serve-static";function c(){let e=r.resolve(import.meta.dirname,`../web`);n(r.join(e,`index.html`))||(console.error(`Web UI assets missing. Please reinstall Foundry: npm install -g @dhzh/foundry`),i(1));let t=new a;return t.use(`/*`,s({root:e})),{server:o({fetch:t.fetch,hostname:`127.0.0.1`,port:7777}),url:`http://127.0.0.1:7777`}}const l=e(`foundry`);l.command(`[...args]`,`Open a local Foundry workspace`).action(()=>{let e=c(),n=t(e.url,e.url,{fallback:!1});console.info(`Foundry workspace is running at ${n}`)}),l.help(),l.parse();export{};
1
+ import e from"cac";import t,{exit as n}from"node:process";import r from"terminal-link";import{z as i}from"zod";import{existsSync as a,mkdirSync as o}from"node:fs";import{DatabaseSync as s}from"node:sqlite";import{homedir as c}from"node:os";import l from"node:path";import u from"cli-table3";import{consola as d}from"consola";import{serve as f}from"@hono/node-server";import{serveStatic as p}from"@hono/node-server/serve-static";import{zValidator as m}from"@hono/zod-validator";import{Hono as h}from"hono";function g(){return l.join(c(),`.foundry`)}function _(){return l.join(g(),`foundry.sqlite`)}function v(){let e=g();return a(e)||o(e,{recursive:!0}),e}function y(){let e=_();return a(e)||new s(e).close(),e}function b(){return{storageRootPath:v(),databasePath:y()}}function x(e){let t=e.prepare(`PRAGMA integrity_check`).all().flatMap(e=>Object.values(e));if(t.length!==1||t[0]!==`ok`)throw Error(`SQLite database integrity check failed`)}function S(e,t){if(!e.isOpen)throw Error(`Cannot start a transaction on a closed database`);if(e.isTransaction)throw Error(`Nested SQLite transactions are not supported`);e.exec(`BEGIN`);try{let n=t();return e.exec(`COMMIT`),n}catch(t){throw e.exec(`ROLLBACK`),t}}function C(){let e=_(),t=new s(e,{enableForeignKeyConstraints:!0,timeout:5e3});t.exec(`
2
+ PRAGMA journal_mode = DELETE;
3
+ PRAGMA synchronous = FULL;
4
+ `);let n=!1;return{database:t,databasePath:e,assertIntegrity:()=>x(t),transaction:e=>S(t,e),close:()=>{n||=(t.close(),!0)}}}const w=[{group:`ui`,name:`theme`,defaultValue:`system`,schema:i.enum([`system`,`light`,`dark`]),secret:!1},{group:`ui`,name:`pointer`,defaultValue:!0,schema:i.boolean(),secret:!1}];function T(e,t){return w.find(n=>n.group===e&&n.name===t)}function E(e,t){return`${e}.${t}`}const D=`settings`;function O(e){e.database.exec(`
5
+ CREATE TABLE IF NOT EXISTS ${D} (
6
+ "group" TEXT NOT NULL,
7
+ name TEXT NOT NULL,
8
+ payload TEXT NOT NULL,
9
+ created_at INTEGER NOT NULL,
10
+ updated_at INTEGER NOT NULL,
11
+ PRIMARY KEY ("group", name)
12
+ )
13
+ `);let t=e.database.prepare(`
14
+ INSERT OR IGNORE INTO ${D} ("group", name, payload, created_at, updated_at)
15
+ VALUES (?, ?, ?, ?, ?)
16
+ `),n=Date.now();e.transaction(()=>{for(let e of w)t.run(e.group,e.name,JSON.stringify({value:e.defaultValue}),n,n)})}function k(e){let t=(e=[])=>`
17
+ SELECT * FROM ${D}
18
+ ${e.length>0?`WHERE ${e.map(e=>`${e} = ?`).join(` AND `)}`:``}
19
+ `,n=e.database.prepare(t()),r=e.database.prepare(t([`"group"`,`name`])),i=e.database.prepare(`
20
+ INSERT INTO ${D} ("group", name, payload, created_at, updated_at)
21
+ VALUES (?, ?, ?, ?, ?)
22
+ ON CONFLICT("group", name) DO UPDATE SET
23
+ payload = excluded.payload,
24
+ updated_at = excluded.updated_at
25
+ `);return{get({group:e,name:t}){return r.get(e,t)},getAll(){return n.all()},upsert(e,t,n){let r=Date.now();return i.run(e,t,n,r,r).changes===1}}}function A(e,t){let n=T(e,t);if(!n)throw Error(`Unknown setting: ${e}.${t}`);return n}function j(e,t){let n;try{n=JSON.parse(e)}catch{return{structurallyValid:!1}}let r=i.object({value:t}).safeParse(n);return r.success?{value:r.data.value,structurallyValid:!0}:{structurallyValid:!1}}function M(e){O(e);let t=k(e);return{get(e,n){let r=t.get({group:e,name:n}),i=A(e,n),a=E(e,n);if(r){let t=j(r.payload,i.schema);if(t.structurallyValid)return{key:a,group:e,name:n,value:t.value}}return{key:a,group:e,name:n,value:i.defaultValue}},list(){return t.getAll().map(({group:e,name:t,payload:n})=>{let r=A(e,t),i=j(n,r.schema);return{key:E(e,t),group:e,name:t,value:i.structurallyValid?i.value:r.defaultValue}})},setMany(n){e.transaction(()=>{for(let{group:e,name:r,value:i}of n)t.upsert(e,r,JSON.stringify({value:i}))})},resetMany(n){let r=n.map(({group:e,name:t})=>A(e,t));e.transaction(()=>{for(let e of r)t.upsert(e.group,e.name,JSON.stringify({value:e.defaultValue}))})}}}function N(){let e=b(),t=C();try{return t.assertIntegrity(),{storage:t,storagePaths:e,settingsService:M(t),close:t.close}}catch(e){throw t.close(),e}}const P={log(...e){d.log(...e)},raw(...e){d.log.raw(...e)},table(e,t){let n=new u({head:[...e]});for(let e of t)n.push([...e]);d.log(n.toString())},info(...e){d.info(...e)},error(...e){d.error(...e)}},F=i.enum([`get`,`list`,`set`,`reset`]),I=[`Key`,`Value`];function L(e){return typeof e==`string`?e:JSON.stringify(e)}function R(e,t,n){if(e.length!==t)throw Error(`Usage: foundry settings ${n}`)}function z(e){let t=e.indexOf(`.`);if(t<=0||t>=e.length-1)throw Error(`Invalid setting key: ${e}`);return{group:e.slice(0,t),name:e.slice(t+1)}}function B(e){e.command(`settings <action> [...values]`,`Get, list, set, or reset Foundry settings`).option(`--raw`,`Print raw output`).action((e,t,n)=>{let r=F.safeParse(e);if(!r.success)throw Error(`Unknown settings action: ${e}, expected one of: ${F.options.join(`, `)}`);let{raw:i=!1}=n,a=N();try{switch(r.data){case`get`:{R(t,1,`get <key>`);let{group:e,name:n}=z(t[0]),r=a.settingsService.get(e,n);i?P.raw(L(r.value)):P.table(I,[[r.key,L(r.value)]]);return}case`list`:{R(t,0,`list`);let e=a.settingsService.list();if(i)for(let t of e)P.raw(L(t.value));else P.table(I,e.map(e=>[e.key,L(e.value)]));return}case`reset`:{R(t,1,`reset <key>`);let{group:e,name:n}=z(t[0]);try{a.settingsService.resetMany([{group:e,name:n}]),i?P.raw(`true`):P.log(!0)}catch{i?P.raw(`false`):P.log(!1)}return}case`set`:{R(t,2,`set <key> <value>`);let{group:e,name:n}=z(t[0]);try{a.settingsService.setMany([{group:e,name:n,value:t[1]}]),i?P.raw(`true`):P.log(!0)}catch{i?P.raw(`false`):P.log(!1)}}}}finally{a.close()}})}const V=e=>new h().get(`/api/settings`,t=>t.json(e.list())).post(`/api/settings`,m(`json`,i.array(i.object({group:i.string().min(1),name:i.string().min(1),value:i.json()})),(e,t)=>{if(!e.success)return t.json({error:`Invalid settings input`,issues:e.error.issues},400)}),t=>{let n=t.req.valid(`json`);try{return e.setMany(n),t.json(!0)}catch{return t.json({error:`Failed to update settings`},500)}}).post(`/api/settings/reset`,m(`json`,i.object({keys:i.array(i.object({group:i.string().min(1),name:i.string().min(1)}))}),(e,t)=>{if(!e.success)return t.json({error:`Invalid settings input`,issues:e.error.issues},400)}),t=>{let n=t.req.valid(`json`);try{return e.resetMany(n.keys),t.json(!0)}catch{return t.json({error:`Failed to reset settings`},500)}});function H(){let e=l.resolve(import.meta.dirname,`../web`);a(l.join(e,`index.html`))||(P.error(`Web UI assets missing. Please reinstall Foundry: npm install -g @dhzh/foundry`),n(1));let t=N(),r=V(t.settingsService);r.use(`/*`,p({root:e}));try{let e=f({fetch:r.fetch,hostname:`127.0.0.1`,port:7777});return{server:e,url:`http://127.0.0.1:7777`,close:()=>{e.close(),t.close()}}}catch(e){throw t.close(),e}}const U=e(`foundry`);B(U),U.command(`[...args]`,`Open a local Foundry workspace`).action(()=>{let e=H(),t=r(e.url,e.url,{fallback:!1});P.info(`Foundry workspace is running at ${t}`)}),U.help();try{U.parse()}catch(e){P.error(e instanceof Error?e.message:String(e)),t.exitCode=1}export{};
@@ -1 +1 @@
1
- <!DOCTYPE html><html><head><title>Foundry</title><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><script defer src="/static/js/lib-react.bf6574ea51.js"></script><script defer src="/static/js/488.ff29f65580.js"></script><script defer src="/static/js/index.8be18902e4.js"></script><link href="/static/css/488.399a9465c7.css" rel="stylesheet"><link href="/static/css/index.ef46db3751.css" rel="stylesheet"></head><body><div id="root"></div></body></html>
1
+ <!DOCTYPE html><html><head><title>Foundry</title><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><script defer src="/static/js/lib-react.8689a73639.js"></script><script defer src="/static/js/lib-router.7aa1b84d62.js"></script><script defer src="/static/js/304.ae01979a4b.js"></script><script defer src="/static/js/index.93ad4f2a33.js"></script><link href="/static/css/304.399a9465c7.css" rel="stylesheet"><link href="/static/css/index.400108c990.css" rel="stylesheet"></head><body><div id="root"></div></body></html>
@@ -0,0 +1 @@
1
+ html,body,#root{overscroll-behavior:none;height:100%;overflow:hidden}.astryx-layout-content,.astryx-layout-panel,.astryx-side-nav>*,.astryx-mobile-nav,.astryx-mobile-nav *{overscroll-behavior:none}