From 6972a004453aa684fcdf2e7147b8b4841925a86c Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 28 Sep 2026 14:20:12 +0800 Subject: [PATCH] feat(terminal): add system commands and DevTools bridge --- CHANGELOG.md | 6 +++++ README.md | 8 +++--- package.json | 2 +- public/addons.js | 2 +- public/i18n-renderers.js | 2 +- public/index.html | 6 ++--- public/locales/en.json | 11 +++++++- public/locales/zh-CN.json | 11 +++++++- public/service-worker.js | 2 +- public/terminal-app.js | 56 ++++++++++++++++++++++++++++++++++++--- scripts/build.mjs | 2 +- scripts/validate.mjs | 31 ++++++++++++++++++++-- 12 files changed, 120 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2d2cb8..52e0923 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ 本项目按 `v主版本.次版本.修订版本` 标记发布。下列内容以对应 Git tag 和已提交功能为准。 +## v1.12.0 — 2026-09-28 + +- 目标与待办可在终端逐项创建;记忆、目标、待办可先选择记录,再输入 `yes` 确认删除。 +- 终端打开时显示欢迎信息,增加日期、时间、会话运行时间、版本、统计和命令历史等常用命令。 +- 开放 `LifeOS.run()`、`LifeOS.note()` 等浏览器开发者控制台入口,复用终端命令与删除确认;图形界面和终端共用删除操作。 + ## v1.11.0 — 2026-09-28 - `memory add` 增加交互式逐项输入,可依次填写标题、日期、标签、能量、天气、地点、关联待办和正文。 diff --git a/README.md b/README.md index b6e578c..593e761 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 以桌面操作系统为交互方式的个人记录网站。用 App 记录日常、管理目标与待办,并沿时间线回看已经留下的内容。 -在线站点:[Life OS](https://life-os-personal-desktop.biss0.chatgpt.site) · 当前应用版本:**v1.11.0** · [更新日志](CHANGELOG.md) +在线站点:[Life OS](https://life-os-personal-desktop.biss0.chatgpt.site) · 当前应用版本:**v1.12.0** · [更新日志](CHANGELOG.md) ## 功能 @@ -16,14 +16,16 @@ | 洞察 | 周报和月报展示记录频率、完成任务数、常见天气、能量记录及收藏或标记高光的记忆;仅依据已保存的数据统计。 | | 档案 | 查看记录概况并维护个人资料。 | | 浏览器 | 在桌面窗口里打开网页或使用 Google 搜索,支持后退、前进、刷新和外部打开。默认打开 Google;第三方网站可能禁止 iframe 嵌入。 | -| 终端 | 用交互式命令创建、查询记忆、目标与待办,完成或重新开启待办,并打开其他 App。 | +| 终端 | 逐项创建记忆、目标与待办;选择并确认删除记录,管理完成状态,查询数据和打开其他 App。 | | 系统设置 | 选择语言、主题、壁纸、桌面快捷方式和开机 App,管理备份。 | 桌面端支持单击选择、双击打开 App、多窗口、任务栏恢复、图标拖动排列和窗口布局记忆。拖动图标或小部件时会显示吸附网格;目标倒计时、时钟、今日概览与快捷操作小部件可移动、调整大小和分别开关。移动端以横排图标和全屏 App 为主,小部件在桌面下方排列。可选必应每日图片、内置背景或自定义 HTTPS 图片链接。中文与英文可自动跟随浏览器语言,也可手动选择。 快捷键:`Ctrl / ⌘ + K` 打开启动器,`Ctrl / ⌘ + F` 搜索记忆,`Ctrl / ⌘ + S` 保存今日记忆;`Alt + 1–9` 打开原有 App,`Alt + 0` 打开终端。终端内 ↑↓ 浏览命令历史,Tab 补全命令,记忆正文输入时 `Ctrl / ⌘ + Enter` 保存。站点包含 PWA 清单和 Service Worker,离线时可打开已缓存的应用外壳;云同步、外部网页与必应图片仍需要联网。 -终端输入 `help` 查看命令。直接输入 `memory add` 会依次询问标题、日期、标签、能量、天气、地点、关联待办与正文;`memory add "实验记录"` 可跳过标题,回车采用默认值,`back` 返回上一步,`cancel` 取消。仍可用参数快速输入,例如 `memory add "实验记录" --date 2026-09-28 --tag 学习 --text "完成实验"`。`task add "背单词" --due tomorrow --priority high` 新增待办,`goal add "准备六级" --due 2026-12-12 --pin` 新增并在桌面显示倒计时。命令只执行明确支持的 Life OS 操作,不是系统 shell;同一天已有记忆时,终端会提示在“今日”或“时间线”编辑。 +终端首次打开显示欢迎信息,输入 `help` 查看命令。`memory add`、`goal add`、`task add` 可逐项创建;回车保留默认值,`back` 返回,`cancel` 取消。`memory delete`、`goal delete`、`task delete` 会列出记录供选择,并要求输入 `yes` 确认。原有带参数的快速输入仍可用,例如 `memory add "实验记录" --date 2026-09-28 --tag 学习 --text "完成实验"`、`task add "背单词" --due tomorrow --priority high`。`date`、`time`、`datetime` 读取当前设备时间;另有 `uptime`、`status`、`apps`、`history`、`echo`、`version` 等命令。终端只执行明确支持的 Life OS 操作,不是系统 shell。 + +浏览器开发者工具的 Console 也可使用同一套命令:`LifeOS.help()`、`LifeOS.run('date')`、`LifeOS.run('memory add')`;逐项填写后用 `LifeOS.note('正文')` 保存记忆,或用 `LifeOS.cancel()` 取消。该入口不会替换或拦截浏览器原生的 `console`,删除操作同样需要在后续调用 `LifeOS.run('yes')` 确认。 ## 数据与备份 diff --git a/package.json b/package.json index ad7b253..c64106c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "life-os", - "version": "1.11.0", + "version": "1.12.0", "private": true, "type": "module", "scripts": { diff --git a/public/addons.js b/public/addons.js index ce53214..a292b78 100644 --- a/public/addons.js +++ b/public/addons.js @@ -3,7 +3,7 @@ const DRAFT_KEY = 'lifeos_today_draft_v1'; const localDay = () => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; }; const escapeHtml = value => String(value || '').replace(/[&<>]/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); - const APP_VERSION = '1.11.0'; + const APP_VERSION = '1.12.0'; window.LifeOSVersion = APP_VERSION; const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system', 'browser', 'terminal']; const t = (key, values) => LifeI18n.t(key, values); diff --git a/public/i18n-renderers.js b/public/i18n-renderers.js index cdeecd8..7aa6d31 100644 --- a/public/i18n-renderers.js +++ b/public/i18n-renderers.js @@ -110,7 +110,7 @@ window.saveProfile = () => { save('lifeos_profile', { name: pname.value, birthday: pbday.value, startDate: pstart.value || day(), motto: pmotto.value }); renderAll(); toast(t('profile.saved')); }; window.systemApp = container => { const state = settings(), preference = LifeI18n.preference(), bytes = new Blob([JSON.stringify({ mems: mems(), museum: museum(), profile: profile(), system: sys() })]).size, oldBackup = state.lastBackup && Date.now() - new Date(state.lastBackup) > 12096e5, detected = browserLanguage() === 'zh' ? t('system.detectedZh') : t('system.detectedEn'); - container.innerHTML = `
${t('system.eyebrow')}

${t('system.title')}

${t('system.appearance')}
${['dark','light','auto'].map(item => ``).join('')}
${t('system.language')}

${detected}

${t('system.data')}
${t('system.storage')}

${t('system.local')}
${t('system.storageSummary',{memories:LifeI18n.number(mems().length),museum:LifeI18n.number(museum().length),tasks:LifeI18n.number(sys().tasksBoard?.tasks?.length||0),size:LifeI18n.number(Math.ceil(bytes/1024))})}

${t('system.dataStorage')}

${t('system.stored')}
${t('system.backups')}

${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `
${t('system.recommended')}` : ''}

${t('system.version')}

${t('system.release',{version:window.LifeOSVersion||'1.11.0'})}

`; + container.innerHTML = `
${t('system.eyebrow')}

${t('system.title')}

${t('system.appearance')}
${['dark','light','auto'].map(item => ``).join('')}
${t('system.language')}

${detected}

${t('system.data')}
${t('system.storage')}

${t('system.local')}
${t('system.storageSummary',{memories:LifeI18n.number(mems().length),museum:LifeI18n.number(museum().length),tasks:LifeI18n.number(sys().tasksBoard?.tasks?.length||0),size:LifeI18n.number(Math.ceil(bytes/1024))})}

${t('system.dataStorage')}

${t('system.stored')}
${t('system.backups')}

${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `
${t('system.recommended')}` : ''}

${t('system.version')}

${t('system.release',{version:window.LifeOSVersion||'1.12.0'})}

`; }; window.importPreview = () => { const data = importData, board = data.system?.tasksBoard || {}; rootModal().innerHTML = ``; }; window.replaceConfirm = () => { rootModal().innerHTML = ``; }; diff --git a/public/index.html b/public/index.html index dde0949..c6c2b7e 100644 --- a/public/index.html +++ b/public/index.html @@ -14,7 +14,7 @@ function browserLanguage(){let ls=navigator.languages?.length?navigator.language function appName(id){return i18n(APPS[id][1])}function moodName(m){return i18n(m.toUpperCase(),cap(m))}function day(){const d=new Date();return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`}function fmtDate(d){return new Date(d+'T12:00:00').toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric'})}function shortDate(d){return new Date(d+'T12:00:00').toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',lang()==='zh'?{month:'numeric',day:'numeric'}:{month:'short',day:'numeric'}).toUpperCase()}function cap(x){return x[0].toUpperCase()+x.slice(1)}function esc(s){return (s||'').replace(/[&<>]/g,x=>({'&':'&','<':'<','>':'>'}[x]))} function applyWallpaper(w){if(!w?.url)return;desktop.style.backgroundImage=`linear-gradient(120deg,color-mix(in srgb,var(--bg) 68%,transparent),color-mix(in srgb,var(--bg) 36%,transparent)),url('${w.url}')`;desktop.style.backgroundSize='cover';desktop.style.backgroundPosition='center';desktop.title=LifeI18n.t('shell.wallpaperTitle',{title:w.title||LifeI18n.t('wallpaper.dailyImage')})}async function loadBingWallpaper(){let s=settings();if(s.wallpaper?.date===day()&&s.wallpaper?.source==='lifeos-proxy'){applyWallpaper(s.wallpaper);return}try{let r=await fetch('/api/bing-wallpaper'),d=await r.json();if(!r.ok||!d.url)throw Error(d.error||'No Bing image');s=settings();if((s.wallpaperSource||'bing')!=='bing')return;s.wallpaper={date:day(),url:'/api/bing-wallpaper-image?date='+day(),title:d.title||'Bing daily image',source:'lifeos-proxy'};save('lifeos_settings',s);applyWallpaper(s.wallpaper)}catch(e){console.info('Bing wallpaper unavailable; using the calm fallback.',e)}}function init(){let s=settings();document.documentElement.dataset.theme=s.theme==='auto'?(matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'):s.theme;renderIcons();renderTaskbar();loadBingWallpaper();setInterval(clock,1000);clock();setTimeout(()=>window.applyLanguage?.(),0);setTimeout(()=>boot.remove(),650)} function desktopIconClick(id,e){if(innerWidth<650){openApp(id);return}selectedIcon=id;document.querySelectorAll('.app-icon').forEach(x=>x.classList.remove('selected'));e.currentTarget.classList.add('selected')}function desktopIconKey(id,e){if(e.key==='Enter'||e.key===' '){e.preventDefault();openApp(id)}}function renderIcons(){icons.innerHTML=Object.entries(APPS).map(([id,[g,n]])=>``).join('')}function taskClick(id){let w=windows[id];if(!w){openApp(id);return}if(w.classList.contains('min')){openApp(id);return}if(activeWindow===id){minWin(id);return}focus(w)}function renderTaskbar(){let ids=innerWidth<650?Object.keys(windows):['today','timeline','museum','insights','tasks','calendar','browser','terminal'];tasks.innerHTML=ids.map(id=>{let w=windows[id],min=w?.classList.contains('min'),fg=activeWindow===id&&!min,name=window.appName(id);return``}).join('')} -function clock(){let d=new Date();document.querySelector('#clock').textContent=d.toLocaleTimeString(LifeI18n.locale(), {hour:'2-digit',minute:'2-digit'});document.querySelector('#date').textContent=d.toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',lang()==='zh'?{month:'numeric',day:'numeric'}:{month:'short',day:'numeric'}).toUpperCase()}function toggleLauncher(){launcher.classList.toggle('hidden');calendar.classList.add('hidden');launcher.innerHTML=`
LIFE OS${LifeI18n.t(`shell.${new Date().getHours()<12?'morning':new Date().getHours()<18?'afternoon':'evening'}`)}
${Object.entries(APPS).map(([id,a])=>``).join('')}
${LifeI18n.t('shell.systemOnline',{version:window.LifeOSVersion||'1.11.0'})}
`}function toggleCalendar(){calendar.classList.toggle('hidden');launcher.classList.add('hidden');calendar.innerHTML=`
${LifeI18n.t('shell.systemDate')}

${fmtDate(day())}

${LifeI18n.t('shell.todaySummary',{memories:mems().length,preserved:museum().length})}

`}function hideContext(){document.querySelector('#life-context')?.remove()}function showContext(e,id){e.preventDefault();e.stopPropagation();hideContext();let labels={open:LifeI18n.t('shell.open'),min:LifeI18n.t('shell.minimize'),close:LifeI18n.t('shell.menuCloseWindow'),refresh:LifeI18n.t('shell.refresh'),new:LifeI18n.t('shell.newToday'),system:LifeI18n.t('shell.systemSettings')},actions=id?`${windows[id]?``:''}`:`
`;let m=document.createElement('div');m.id='life-context';m.style.cssText=`position:fixed;z-index:7000;left:${Math.min(e.clientX,innerWidth-190)}px;top:${Math.min(e.clientY,innerHeight-150)}px;width:180px;padding:6px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow)`;m.innerHTML=actions;m.querySelectorAll('button').forEach(b=>b.style.cssText='display:block;width:100%;border:0;background:transparent;color:var(--ink);text-align:left;border-radius:6px;padding:9px 10px;font:12px Manrope');m.querySelectorAll('button').forEach(b=>b.onmouseenter=()=>b.style.background='var(--panel2)');document.body.append(m)}document.addEventListener('contextmenu',e=>{if(e.target.closest('.desktop')&&!e.target.closest('.app-icon')&&!e.target.closest('.window'))showContext(e,null)});document.addEventListener('click',hideContext) +function clock(){let d=new Date();document.querySelector('#clock').textContent=d.toLocaleTimeString(LifeI18n.locale(), {hour:'2-digit',minute:'2-digit'});document.querySelector('#date').textContent=d.toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',lang()==='zh'?{month:'numeric',day:'numeric'}:{month:'short',day:'numeric'}).toUpperCase()}function toggleLauncher(){launcher.classList.toggle('hidden');calendar.classList.add('hidden');launcher.innerHTML=`
LIFE OS${LifeI18n.t(`shell.${new Date().getHours()<12?'morning':new Date().getHours()<18?'afternoon':'evening'}`)}
${Object.entries(APPS).map(([id,a])=>``).join('')}
${LifeI18n.t('shell.systemOnline',{version:window.LifeOSVersion||'1.12.0'})}
`}function toggleCalendar(){calendar.classList.toggle('hidden');launcher.classList.add('hidden');calendar.innerHTML=`
${LifeI18n.t('shell.systemDate')}

${fmtDate(day())}

${LifeI18n.t('shell.todaySummary',{memories:mems().length,preserved:museum().length})}

`}function hideContext(){document.querySelector('#life-context')?.remove()}function showContext(e,id){e.preventDefault();e.stopPropagation();hideContext();let labels={open:LifeI18n.t('shell.open'),min:LifeI18n.t('shell.minimize'),close:LifeI18n.t('shell.menuCloseWindow'),refresh:LifeI18n.t('shell.refresh'),new:LifeI18n.t('shell.newToday'),system:LifeI18n.t('shell.systemSettings')},actions=id?`${windows[id]?``:''}`:`
`;let m=document.createElement('div');m.id='life-context';m.style.cssText=`position:fixed;z-index:7000;left:${Math.min(e.clientX,innerWidth-190)}px;top:${Math.min(e.clientY,innerHeight-150)}px;width:180px;padding:6px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow)`;m.innerHTML=actions;m.querySelectorAll('button').forEach(b=>b.style.cssText='display:block;width:100%;border:0;background:transparent;color:var(--ink);text-align:left;border-radius:6px;padding:9px 10px;font:12px Manrope');m.querySelectorAll('button').forEach(b=>b.onmouseenter=()=>b.style.background='var(--panel2)');document.body.append(m)}document.addEventListener('contextmenu',e=>{if(e.target.closest('.desktop')&&!e.target.closest('.app-icon')&&!e.target.closest('.window'))showContext(e,null)});document.addEventListener('click',hideContext) function openApp(id){launcher.classList.add('hidden');let w=windows[id];if(w){w.classList.remove('min');focus(w);return}w=document.createElement('section');w.className='window';w.dataset.id=id;w.style.left=(innerWidth<650?0:90+Object.keys(windows).length*36)+'px';w.style.top=(innerWidth<650?0:72+Object.keys(windows).length*28)+'px';w.innerHTML=`
${APPS[id][0]}${appName(id)}
`;windows[id]=w;document.querySelector('#windows').append(w);let controls=w.querySelector('.controls'),bar=w.querySelector('.titlebar');controls.addEventListener('pointerdown',e=>{e.preventDefault();e.stopPropagation()});controls.addEventListener('click',e=>{let action=e.target.closest('button')?.dataset.windowAction;if(!action)return;e.preventDefault();e.stopPropagation();if(action==='min')minWin(id);if(action==='max')maxWin(id);if(action==='close')closeWin(id)});bar.addEventListener('dblclick',e=>{if(!e.target.closest('.controls'))maxWin(id)});w.addEventListener('pointerdown',()=>focus(w));drag(w);renderApp(id);focus(w)}function focus(w){if(!w)return;activeWindow=w.dataset.id;w.style.zIndex=++z;document.querySelectorAll('.window').forEach(x=>x.classList.toggle('active-window',x===w));renderTaskbar()}function closeWin(id){windows[id]?.remove();delete windows[id];if(activeWindow===id){activeWindow=Object.keys(windows).find(k=>!windows[k].classList.contains('min'))||null;if(activeWindow)focus(windows[activeWindow])}renderTaskbar()}function minWin(id){let w=windows[id];if(!w)return;w.classList.add('min');if(activeWindow===id){activeWindow=Object.keys(windows).find(k=>k!==id&&!windows[k].classList.contains('min'))||null;if(activeWindow)focus(windows[activeWindow])}renderTaskbar();toast(i18n('MINIMIZED — TAP THE APP ICON TO RESTORE'))}function maxWin(id){let w=windows[id];if(!w)return;w.classList.toggle('max');focus(w)}function snapWindow(w){if(innerWidth<650||w.classList.contains('max'))return;let r=w.getBoundingClientRect(),edge=22;if(r.topinnerWidth-edge){w.style.left='calc(50vw + 9px)';w.style.top='18px';w.style.width='calc(50vw - 27px)';w.style.height='calc(100vh - 98px)'}}function drag(w){let bar=w.querySelector('.titlebar'),x,y,l,t;bar.onpointerdown=e=>{if(e.target.closest('.controls')||innerWidth<650)return;if(w.classList.contains('max'))w.classList.remove('max');x=e.clientX;y=e.clientY;l=w.offsetLeft;t=w.offsetTop;bar.setPointerCapture(e.pointerId);bar.onpointermove=e=>{w.style.left=Math.max(0,l+e.clientX-x)+'px';w.style.top=Math.max(0,t+e.clientY-y)+'px'};bar.onpointerup=()=>{bar.onpointermove=null;snapWindow(w)}}}document.addEventListener('keydown',e=>{if(e.altKey&&e.key==='F4'&&activeWindow){e.preventDefault();closeWin(activeWindow)}if(e.key==='Escape'&&activeWindow&&!document.querySelector('.modal')&&!document.querySelector('#lifeos-search')){let w=windows[activeWindow];if(w?.classList.contains('max')){e.preventDefault();maxWin(activeWindow)}}}) function renderApp(id){let c=document.querySelector('#content-'+id);if(!c)return;if(id==='today')window.today(c);if(id==='timeline')window.timeline(c);if(id==='museum')window.museumApp(c);if(id==='insights')window.insights(c);if(id==='profile')window.profileApp(c);if(id==='tasks')window.tasksApp(c);if(id==='calendar')window.calendarApp(c);if(id==='system')window.systemApp(c);if(id==='browser')window.browserApp(c);if(id==='terminal')window.terminalApp(c);setTimeout(()=>window.applyLanguage?.(),0)}function renderAll(){Object.keys(windows).forEach(renderApp);renderTaskbar();window.renderDesktopGoalCountdowns?.();setTimeout(()=>window.applyLanguage?.(),0)}document.addEventListener('click',e=>{if(e.target.closest('#content-today .chip,#content-today .energy button'))setTimeout(()=>window.applyLanguage?.(),0)}) let todayDraft=null,todayTitleDraft=null,todayTargetDate=null;function today(c){let m=mems().find(x=>x.date===day());if(todayDraft===null){selectedMood=m?.mood||'cloudy';selectedEnergy=m?.energy||3}let f=m||filmFor(selectedMood,selectedEnergy),text=todayDraft===null?(m?.text||''):todayDraft;c.innerHTML=`
${new Date().toLocaleDateString('en-US',{weekday:'long'}).toUpperCase()}

${fmtDate(day()).replace(/^[^,]+, /,'')}

How was your day?

`}function filmFor(m,e){const dict=LifeI18n.dictionaries[lang()==='zh'?'zh':'en'],seed=[...`${day()}|${m}|${e}`].reduce((sum,char,index)=>sum+char.charCodeAt(0)*(index+1),0),n=seed%dict.prompt.films.length;return{filmTitle:dict.prompt.films[n].title,filmSubtitle:dict.prompt.films[n].subtitle,filmLine:dict.prompt.lines[(n*3+e)%dict.prompt.lines.length]}}function rerenderToday(){let old=document.querySelector('#entry');todayDraft=old?old.value:todayDraft||'';window.today(document.querySelector('#content-today'));let entry=document.querySelector('#entry');entry?.focus();entry?.setSelectionRange(entry.value.length,entry.value.length)}function setMood(x){selectedMood=x;rerenderToday()}function setEnergy(x){selectedEnergy=x;rerenderToday()}function archiveToday(){let text=document.querySelector('#entry').value.trim(),a=mems(),old=a.find(x=>x.date===day()),f=old||filmFor(selectedMood,selectedEnergy),now=new Date().toISOString(),m={id:old?.id||'memory_'+crypto.randomUUID(),date:day(),text,mood:selectedMood,energy:selectedEnergy,filmTitle:f.filmTitle,filmSubtitle:f.filmSubtitle,filmLine:f.filmLine,createdAt:old?.createdAt||now,updatedAt:now};let status=document.querySelector('#saveStatus');status.textContent='ARCHIVING MEMORY...';setTimeout(()=>{save('lifeos_memories',[...a.filter(x=>x.date!==day()),m].sort((x,y)=>y.date.localeCompare(x.date)));todayDraft=null;status.textContent='MEMORY ARCHIVED';renderAll()},360)} @@ -23,6 +23,6 @@ function museumApp(c){let a=museum(),ms=mems();c.innerHTML=`
m.date===d.toISOString().slice(0,10))){s++;d.setDate(d.getDate()-1)}return s}function longest(a){let ds=a.map(x=>x.date).sort(),best=0,run=0,last='';ds.forEach(d=>{run=last&&((new Date(d)-new Date(last))/864e5===1)?run+1:1;best=Math.max(best,run);last=d});return best}function insights(c){let a=mems(),last=a.filter(m=>(Date.now()-new Date(m.date+'T12:00:00'))<2592e6),cnt={clear:0,light:0,cloudy:0,rain:0,storm:0};last.forEach(x=>cnt[x.mood]++);let avg=a.length?(a.reduce((s,x)=>s+x.energy,0)/a.length).toFixed(1):'—',days={};a.forEach(x=>{let d=new Date(x.date+'T12:00:00').toLocaleDateString('en-US',{weekday:'long'});days[d]=(days[d]||0)+1});let active=Object.entries(days).sort((x,y)=>y[1]-x[1])[0]?.[0]||'—';c.innerHTML=`
LAST 30 DAYS

Insights, quietly.

WEATHER
${Object.entries(cnt).map(([k,v])=>`
${cap(k)} ${v} DAYS
`).join('')}
AVERAGE ENERGY
${avg} / 5
${a.slice(0,10).reverse().map(x=>``).join('')||'No entries yet'}
MOST ACTIVE DAY
${active}
CURRENT STREAK
${streak(a)} DAYS
Longest streak: ${longest(a)} days
`} function profileApp(c){let p=profile(),a=mems(),common=Object.entries(a.reduce((o,m)=>(o[m.mood]=(o[m.mood]||0)+1,o),{})).sort((x,y)=>y[1]-x[1])[0]?.[0]||'—',avg=a.length?(a.reduce((x,m)=>x+m.energy,0)/a.length).toFixed(1):'—',started=Math.max(1,Math.floor((Date.now()-new Date(p.startDate))/864e5));c.innerHTML=`
SYSTEM PROFILE

${p.name?esc(p.name)+' · ':''}LIFE OS ONLINE

${started}DAYS ONLINE
${a.length}MEMORIES
${museum().length}MUSEUM ITEMS
${streak(a)}CURRENT STREAK
${longest(a)}LONGEST STREAK
${cap(common)}COMMON WEATHER
PERSONAL SYSTEM
AVERAGE ENERGY ${avg} / 5
`}function saveProfile(){save('lifeos_profile',{name:pname.value,birthday:pbday.value,startDate:pstart.value||day(),motto:pmotto.value});renderAll();toast('PROFILE SAVED')} function today(c){let m=mems().find(x=>x.date===day());if(todayDraft===null){selectedMood=m?.mood||'cloudy';selectedEnergy=m?.energy||3}let f=m||filmFor(selectedMood,selectedEnergy),text=todayDraft===null?(m?.text||''):todayDraft,cWeek=new Date().toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',{weekday:'long'}).toUpperCase();c.innerHTML=`
${cWeek}

${fmtDate(day()).replace(/^[^,]+, /,'')}

${i18n('How was your day?')}

`}function rerenderToday(){let old=document.querySelector('#entry');todayDraft=old?old.value:todayDraft||'';window.today(document.querySelector('#content-today'));let entry=document.querySelector('#entry');entry?.focus();entry?.setSelectionRange(entry.value.length,entry.value.length)} -function systemApp(c){let s=settings(),bytes=new Blob([JSON.stringify({mems:mems(),museum:museum(),profile:profile()})]).size;let rec=s.lastBackup&&(Date.now()-new Date(s.lastBackup)>12096e5);c.innerHTML=`
SYSTEM SETTINGS

System, local and yours.

APPEARANCE
${['dark','light','auto'].map(x=>``).join('')}
LANGUAGE
DATA
STORAGE

Local Storage
${mems().length} memories · ${museum().length} museum items · Approx. ${Math.ceil(bytes/1024)} KB

DATA STORAGE

Your data is stored locally in this browser.
Create regular backups to prevent data loss.

${s.lastBackup?'LAST BACKUP · '+fmtDate(s.lastBackup.slice(0,10)):'LAST BACKUP · NEVER'} ${rec?'
BACKUP RECOMMENDED':''}

VERSION

Life OS
Version ${window.LifeOSVersion||'1.11.0'} · Schema v2

`}function theme(x){let s=settings();s.theme=x;save('lifeos_settings',s);init();renderAll()}function exportData(){let data={app:'Life OS',version:window.LifeOSVersion||'1.11.0',schemaVersion:1,exportedAt:new Date().toISOString(),profile:profile(),memories:mems(),museum:museum(),settings:settings()};let a=document.createElement('a');a.href=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'}));a.download='life-os-backup-'+day()+'.json';a.click();let s=settings();s.lastBackup=new Date().toISOString();save('lifeos_settings',s);renderAll();toast('BACKUP CREATED')}function readImport(el){let f=el.files[0];if(!f)return;let r=new FileReader;r.onload=()=>{try{let d=JSON.parse(r.result);if(d.app!=='Life OS'||![1,2].includes(d.schemaVersion)||!Array.isArray(d.memories)||!Array.isArray(d.museum))throw 0;importData=d;importPreview()}catch{toast('INVALID BACKUP FILE')}};r.readAsText(f)}function importPreview(){let d=importData;modal.innerHTML=``}function replaceConfirm(){modal.innerHTML=``}function applyImport(mode){let d=importData;if(mode==='replace'){save('lifeos_memories',d.memories);save('lifeos_museum',d.museum);save('lifeos_profile',d.profile||{});save('lifeos_settings',d.settings||{})}else{let a=mems(), ids=new Set(a.map(x=>x.id)),dates=new Set(a.map(x=>x.date));save('lifeos_memories',[...a,...d.memories.filter(x=>!ids.has(x.id)&&!dates.has(x.date))]);let mu=museum(),mi=new Set(mu.map(x=>x.id));save('lifeos_museum',[...mu,...d.museum.filter(x=>!mi.has(x.id))])}modal.innerHTML='';toast('RESTORING SYSTEM...');setTimeout(()=>{renderAll();toast('SYSTEM RESTORED')},500)}function resetConfirm(){modal.innerHTML=``}function toast(t){let x=document.createElement('div');x.className='toast';x.textContent=t;document.body.append(x);setTimeout(()=>x.remove(),2000)} +function systemApp(c){let s=settings(),bytes=new Blob([JSON.stringify({mems:mems(),museum:museum(),profile:profile()})]).size;let rec=s.lastBackup&&(Date.now()-new Date(s.lastBackup)>12096e5);c.innerHTML=`
SYSTEM SETTINGS

System, local and yours.

APPEARANCE
${['dark','light','auto'].map(x=>``).join('')}
LANGUAGE
DATA
STORAGE

Local Storage
${mems().length} memories · ${museum().length} museum items · Approx. ${Math.ceil(bytes/1024)} KB

DATA STORAGE

Your data is stored locally in this browser.
Create regular backups to prevent data loss.

${s.lastBackup?'LAST BACKUP · '+fmtDate(s.lastBackup.slice(0,10)):'LAST BACKUP · NEVER'} ${rec?'
BACKUP RECOMMENDED':''}

VERSION

Life OS
Version ${window.LifeOSVersion||'1.12.0'} · Schema v2

`}function theme(x){let s=settings();s.theme=x;save('lifeos_settings',s);init();renderAll()}function exportData(){let data={app:'Life OS',version:window.LifeOSVersion||'1.12.0',schemaVersion:1,exportedAt:new Date().toISOString(),profile:profile(),memories:mems(),museum:museum(),settings:settings()};let a=document.createElement('a');a.href=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'}));a.download='life-os-backup-'+day()+'.json';a.click();let s=settings();s.lastBackup=new Date().toISOString();save('lifeos_settings',s);renderAll();toast('BACKUP CREATED')}function readImport(el){let f=el.files[0];if(!f)return;let r=new FileReader;r.onload=()=>{try{let d=JSON.parse(r.result);if(d.app!=='Life OS'||![1,2].includes(d.schemaVersion)||!Array.isArray(d.memories)||!Array.isArray(d.museum))throw 0;importData=d;importPreview()}catch{toast('INVALID BACKUP FILE')}};r.readAsText(f)}function importPreview(){let d=importData;modal.innerHTML=``}function replaceConfirm(){modal.innerHTML=``}function applyImport(mode){let d=importData;if(mode==='replace'){save('lifeos_memories',d.memories);save('lifeos_museum',d.museum);save('lifeos_profile',d.profile||{});save('lifeos_settings',d.settings||{})}else{let a=mems(), ids=new Set(a.map(x=>x.id)),dates=new Set(a.map(x=>x.date));save('lifeos_memories',[...a,...d.memories.filter(x=>!ids.has(x.id)&&!dates.has(x.date))]);let mu=museum(),mi=new Set(mu.map(x=>x.id));save('lifeos_museum',[...mu,...d.museum.filter(x=>!mi.has(x.id))])}modal.innerHTML='';toast('RESTORING SYSTEM...');setTimeout(()=>{renderAll();toast('SYSTEM RESTORED')},500)}function resetConfirm(){modal.innerHTML=``}function toast(t){let x=document.createElement('div');x.className='toast';x.textContent=t;document.body.append(x);setTimeout(()=>x.remove(),2000)} window.renderIcons=renderIcons;window.renderAll=renderAll;window.toast=toast; - + diff --git a/public/locales/en.json b/public/locales/en.json index 3a6833f..ff5b364 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -601,6 +601,15 @@ "confirmCreate": "Create {kind}: {title}? Type yes to save or no to cancel.", "cancelled": "Operation cancelled.", "deleted": "Deleted {kind}: {title}" - } + }, + "welcome": "Life OS v{version} · Terminal ready.\nType help for commands or memory add to create a memory step by step.\ndate / time / status show basic information. Browser DevTools: LifeOS.help().", + "uptime": "Session uptime: {minutes}m {seconds}s", + "status": "{memories} memories · {goals} goals · {tasks} tasks", + "consoleHelp": "Open your browser DevTools console and call LifeOS.help(), LifeOS.run(\"date\"), or LifeOS.run(\"memory add\"). After memory questions, use LifeOS.note(\"your note\"); LifeOS.cancel() stops a flow.", + "consoleReady": "DevTools bridge ready. Type LifeOS.help() to see commands.", + "consoleNotePrompt": "Enter the note with LifeOS.note(\"your text\") or cancel with LifeOS.cancel().", + "noNote": "No memory note is waiting. Start with LifeOS.run(\"memory add\").", + "cleared": "Terminal output cleared.", + "pinNeedsDate": "Set a target date before showing a desktop countdown." } } diff --git a/public/locales/zh-CN.json b/public/locales/zh-CN.json index 1464951..0c3bdab 100644 --- a/public/locales/zh-CN.json +++ b/public/locales/zh-CN.json @@ -601,6 +601,15 @@ "confirmCreate": "确定创建{kind}「{title}」?输入 yes 保存,或 no 取消。", "cancelled": "已取消操作。", "deleted": "已删除{kind}:{title}" - } + }, + "welcome": "Life OS v{version} · 终端已就绪。\n输入 help 查看命令,或输入 memory add 逐项创建记忆。\ndate / time / status 查看基本信息。浏览器开发者控制台可输入 LifeOS.help()。", + "uptime": "本次页面会话已运行:{minutes} 分 {seconds} 秒", + "status": "记忆 {memories} 条 · 目标 {goals} 个 · 待办 {tasks} 项", + "consoleHelp": "打开浏览器开发者工具的 Console,输入 LifeOS.help()、LifeOS.run(\"date\") 或 LifeOS.run(\"memory add\")。完成记忆提问后用 LifeOS.note(\"正文\") 保存;LifeOS.cancel() 取消。", + "consoleReady": "开发者控制台入口已就绪,输入 LifeOS.help() 查看命令。", + "consoleNotePrompt": "使用 LifeOS.note(\"正文\") 输入记忆内容,或使用 LifeOS.cancel() 取消。", + "noNote": "当前没有待填写的记忆正文,请先运行 LifeOS.run(\"memory add\")。", + "cleared": "已清空终端输出。", + "pinNeedsDate": "请先设置目标日期,再在桌面显示倒计时。" } } diff --git a/public/service-worker.js b/public/service-worker.js index fcb5fb1..26b923e 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const CACHE = 'life-os-shell-v1.11.0'; +const CACHE = 'life-os-shell-v1.12.0'; const SHELL = [ '/', '/index.html', '/manifest.webmanifest', '/life-os-icon.svg' ]; diff --git a/public/terminal-app.js b/public/terminal-app.js index b5edff7..db2b6aa 100644 --- a/public/terminal-app.js +++ b/public/terminal-app.js @@ -2,6 +2,7 @@ (() => { const t = (key, values) => LifeI18n.t(`terminal.${key}`, values); const actions = () => window.LifeOSActions; + const startedAt = Date.now(); const localDate = offset => { const value = new Date(); value.setDate(value.getDate() + offset); return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`; }; const date = value => value === 'today' ? localDate(0) : value === 'tomorrow' ? localDate(1) : value || ''; const validDay = value => { const parsed = new Date(`${value}T12:00:00`); return /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(parsed.getTime()) && `${parsed.getFullYear()}-${String(parsed.getMonth() + 1).padStart(2, '0')}-${String(parsed.getDate()).padStart(2, '0')}` === value; }; @@ -43,7 +44,7 @@ }; const board = () => actions().board(); const requireTitle = value => { if (!value?.trim()) throw new Error('usage'); return value.trim(); }; - const help = () => ['help', 'memory add / memory add "…" — ' + t('guidedHelp'), 'memory add "…" [--date YYYY-MM-DD] [--tag …] [--energy 1-5] [--weather cloudy] [--place …] [--task …] [--text …]', 'memory list [--tag …] [--query …]', 'goal add "…" [--due YYYY-MM-DD] [--description …] [--pin]', 'goal list', 'task add "…" [--due tomorrow] [--goal …] [--priority high|normal|low]', 'task list', 'task done <#> / task reopen <#>', 'open / clear'].join('\n'); + const help = () => ['help', 'memory add / memory add "…" — ' + t('guidedHelp'), 'memory add "…" [--date YYYY-MM-DD] [--tag …] [--energy 1-5] [--weather cloudy] [--place …] [--task …] [--text …]', 'memory list [--tag …] [--query …] / memory delete [#|date|title]', 'goal add ["…"] / goal list / goal delete [#|title]', 'goal add "…" --due YYYY-MM-DD [--description …] [--pin]', 'goal done <#> / goal reopen <#>', 'task add ["…"] / task list / task delete [#|title]', 'task add "…" --due tomorrow [--goal …] [--priority high|normal|low]', 'task done <#> / task reopen <#>', 'date / time / datetime / uptime / version', 'status / apps / history / echo "…"', 'open / clear / console'].join('\n'); const wizardFields = ['title', 'date', 'tags', 'energy', 'mood', 'location', 'taskId']; let wizard = null; let flow = null; @@ -98,7 +99,7 @@ if (field === 'title') { if (!value && !draft.title || value.length > (current.kind === 'goal' ? 100 : 140)) throw new Error('invalidTitle'); draft.title = value || draft.title; } if (field === 'description') { if (value.length > 240) throw new Error('invalidValue'); if (value) draft.description = value === '-' ? '' : value; } if (field === 'targetDate' || field === 'dueDate') { const selected = value === '-' ? '' : date(value) || draft[field]; if (selected && !validDay(selected)) throw new Error('invalidDate'); draft[field] = selected; } - if (field === 'showOnDesktop') { if (value && !['yes','y','no','n'].includes(value.toLowerCase())) throw new Error('invalidValue'); draft.showOnDesktop = value ? ['yes','y'].includes(value.toLowerCase()) : draft.showOnDesktop; } + if (field === 'showOnDesktop') { if (value && !['yes','y','no','n'].includes(value.toLowerCase())) throw new Error('invalidValue'); const enabled = value ? ['yes','y'].includes(value.toLowerCase()) : draft.showOnDesktop; if (enabled && !draft.targetDate) throw new Error('pinNeedsDate'); draft.showOnDesktop = enabled; } if (field === 'goalId' && value) draft.goalId = value === '-' ? '' : resolve(board().goals,value); if (field === 'priority') { if (value && !['high','normal','low'].includes(value.toLowerCase())) throw new Error('invalidPriority'); draft.priority = value ? value.toLowerCase() : draft.priority; } current.step++; return flowPrompt(); @@ -143,6 +144,18 @@ if (!kind) return {}; if (kind === 'help') return { message: help() }; if (kind === 'clear') return { clear: true }; + if (kind === 'date' || kind === 'time' || kind === 'datetime') { + if (verb) throw new Error('usage'); + const options = kind === 'date' ? { dateStyle: 'full' } : kind === 'time' ? { timeStyle: 'medium' } : { dateStyle: 'full', timeStyle: 'medium' }; + return { message: new Intl.DateTimeFormat(lang() === 'zh' ? 'zh-CN' : 'en-US', options).format(new Date()) }; + } + if (kind === 'uptime') { if (verb) throw new Error('usage'); const seconds = Math.floor((Date.now() - startedAt) / 1000); return { message: t('uptime', { minutes: Math.floor(seconds / 60), seconds: seconds % 60 }) }; } + if (kind === 'version') { if (verb) throw new Error('usage'); return { message: `Life OS v${window.LifeOSVersion || '1.12.0'}` }; } + if (kind === 'status') { if (verb) throw new Error('usage'); const data = board(); return { message: t('status', { memories: mems().length, goals: data.goals.length, tasks: data.tasks.length }) }; } + if (kind === 'apps') { if (verb) throw new Error('usage'); return { message: Object.keys(APPS).map(id => `${id} — ${LifeI18n.t(`app.${id}`)}`).join('\n') }; } + if (kind === 'history') { if (verb) throw new Error('usage'); return { message: history.slice(-20).map((entry,index) => `${index + 1}. ${entry}`).join('\n') || t('empty') }; } + if (kind === 'echo') return { message: words(line.trim()).slice(1).join(' ') }; + if (kind === 'console') return { message: t('consoleHelp') }; if (kind === 'open') { if (!Object.hasOwn(APPS, verb)) throw new Error('unknownApp'); openApp(verb); return { message: t('opened', { app: LifeI18n.t(`app.${verb}`) }) }; @@ -168,7 +181,7 @@ if (kind === 'memory' && verb === 'delete') return startFlow('memory','delete',rest.join(' ')); if (kind === 'goal' && verb === 'add') { if (!rest.some(part => part.startsWith('--'))) return startFlow('goal','add',rest.join(' ')); - const { flags, values } = options(rest, ['due','description','pin']); const goal = actions().createGoal({ title: requireTitle(values.join(' ')), targetDate: date(flags.due), description: flags.description || '', showOnDesktop: Boolean(flags.pin) }); + const { flags, values } = options(rest, ['due','description','pin']); if (flags.pin && !flags.due) throw new Error('pinNeedsDate'); const goal = actions().createGoal({ title: requireTitle(values.join(' ')), targetDate: date(flags.due), description: flags.description || '', showOnDesktop: Boolean(flags.pin) }); return { message: t('goalSaved', { title: goal.title }) }; } if (kind === 'goal' && verb === 'list') { @@ -205,6 +218,7 @@ window.LifeOSTerminalEngine = { run, words, backStep, finishWizard, cancelSession, active }; const lines = [], history = []; + let welcomed = false; let historyIndex = 0, compose = null, noteDraft = ''; const addLine = (value, kind = 'result') => { lines.push({ value, kind }); if (lines.length > 150) lines.shift(); }; const output = node => { @@ -221,6 +235,7 @@ }; window.terminalApp = node => { if (node.querySelector('.terminal-shell') && node.dataset.terminalLocale === lang()) { output(node); return; } + if (!welcomed) { lines.unshift({ value: t('welcome', { version: window.LifeOSVersion || '1.12.0' }), kind: 'system' }); welcomed = true; } node.dataset.terminalLocale = lang(); node.innerHTML = `
Life OS / ${t('title')}${t('hint')}
`; output(node); composer(node, Boolean(compose)); @@ -244,7 +259,40 @@ node.querySelector('.terminal-note').addEventListener('input', event => { noteDraft = event.currentTarget.value; }); node.querySelector('.terminal-note').addEventListener('keydown', event => { if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { event.preventDefault(); saveNote(); } if (event.key === 'Escape') { event.stopPropagation(); cancelNote(); } }); }; + const consoleOutput = result => { + if (result.clear) lines.length = 0; + if (result.message) addLine(result.message); + if (result.compose) { compose = result.compose; addLine(t('notePrompt')); } + if (result.cancel) { compose = null; noteDraft = ''; } + const node = document.querySelector('#content-terminal'); + if (node?.querySelector('.terminal-shell')) { output(node); if (result.compose || result.cancel) composer(node, Boolean(compose)); } + const message = result.compose ? t('consoleNotePrompt') : result.message || (result.clear ? t('cleared') : ''); + if (message) console.info('[Life OS]', message); + return message; + }; + window.LifeOS = Object.freeze({ + run(line) { + const input = String(line || '').trim(); + if (compose) { console.info('[Life OS]', t('consoleNotePrompt')); return t('consoleNotePrompt'); } + if (!input && !active()) return t('usage'); + try { if (input) history.push(input); addLine(`devtools > ${input || '↵'}`, 'command'); return consoleOutput(run(input)); } + catch (error) { const key = Object.hasOwn(LifeI18n.dictionaries[lang() === 'zh' ? 'zh' : 'en'].terminal,error.message) ? error.message : 'unknownCommand'; return consoleOutput({ message: t(key) }); } + }, + help() { return this.run('help'); }, + note(text) { + if (!compose) { console.warn('[Life OS]', t('noNote')); return t('noNote'); } + try { + const memory = actions().createMemory({ ...compose, text: String(text || '') }); + compose = null; noteDraft = ''; finishWizard(); + const node = document.querySelector('#content-terminal'); + if (node?.querySelector('.terminal-shell')) composer(node, false); + return consoleOutput({ message: t('memorySaved', { title: memory.title, date: memory.date }) }); + } catch (error) { const key = Object.hasOwn(LifeI18n.dictionaries[lang() === 'zh' ? 'zh' : 'en'].terminal,error.message) ? error.message : 'unknownCommand'; return consoleOutput({ message: t(key) }); } + }, + cancel() { cancelSession(); compose = null; noteDraft = ''; return consoleOutput({ message: t('flow.cancelled'), cancel: true }); } + }); + console.info('[Life OS]', t('consoleReady')); const style = document.createElement('style'); - style.textContent = `#content-terminal{padding:0;overflow:hidden}.terminal-shell{height:100%;display:flex;flex-direction:column;background:#111a16;color:#d4efd2;font:13px/1.55 'DM Mono',ui-monospace,monospace}.terminal-header{display:flex;justify-content:space-between;gap:10px;padding:12px 15px;border-bottom:1px solid #375040;color:#a8ed9c}.terminal-header span{color:#94a899;font-size:11px}.terminal-output{flex:1;min-height:0;overflow:auto;padding:15px;white-space:pre-wrap;overflow-wrap:anywhere}.terminal-line{font:inherit;margin:0 0 10px;white-space:pre-wrap}.terminal-line.command{color:#a8ed9c}.terminal-line.error{color:#ff9e99}.terminal-shortcuts{display:flex;gap:6px;overflow:auto;padding:7px 12px;border-top:1px solid #375040}.terminal-shortcuts button,.terminal-compose button,.terminal-form button{border:1px solid #456047;background:#233329;color:#d4efd2;border-radius:7px;padding:5px 9px;white-space:nowrap}.terminal-form{display:flex;align-items:center;gap:8px;border-top:1px solid #375040;padding:10px 14px}.terminal-form label{color:#a8ed9c;white-space:nowrap}.terminal-input{flex:1;min-width:0;background:transparent;color:#f0fff0;border:0;outline:none;font:inherit}.terminal-compose{border-top:1px solid #375040;padding:12px;display:grid;gap:8px}.terminal-compose[hidden],.terminal-form[hidden]{display:none}.terminal-note{width:100%;resize:vertical;background:#1c2a21;color:#f0fff0;border:1px solid #456047;border-radius:7px;padding:10px;font:inherit}.terminal-compose>div{display:flex;gap:8px}@media(max-width:650px){.terminal-header{display:block}.terminal-header span{display:block}.terminal-shell{font-size:12px}}`; + style.textContent = `#content-terminal{padding:0;overflow:hidden}.terminal-shell{height:100%;display:flex;flex-direction:column;background:#111a16;color:#d4efd2;font:13px/1.55 'DM Mono',ui-monospace,monospace}.terminal-header{display:flex;justify-content:space-between;gap:10px;padding:12px 15px;border-bottom:1px solid #375040;color:#a8ed9c}.terminal-header span{color:#94a899;font-size:11px}.terminal-output{flex:1;min-height:0;overflow:auto;padding:15px;white-space:pre-wrap;overflow-wrap:anywhere}.terminal-line{font:inherit;margin:0 0 10px;white-space:pre-wrap}.terminal-line.command,.terminal-line.system{color:#a8ed9c}.terminal-line.error{color:#ff9e99}.terminal-shortcuts{display:flex;gap:6px;overflow:auto;padding:7px 12px;border-top:1px solid #375040}.terminal-shortcuts button,.terminal-compose button,.terminal-form button{border:1px solid #456047;background:#233329;color:#d4efd2;border-radius:7px;padding:5px 9px;white-space:nowrap}.terminal-form{display:flex;align-items:center;gap:8px;border-top:1px solid #375040;padding:10px 14px}.terminal-form label{color:#a8ed9c;white-space:nowrap}.terminal-input{flex:1;min-width:0;background:transparent;color:#f0fff0;border:0;outline:none;font:inherit}.terminal-compose{border-top:1px solid #375040;padding:12px;display:grid;gap:8px}.terminal-compose[hidden],.terminal-form[hidden]{display:none}.terminal-note{width:100%;resize:vertical;background:#1c2a21;color:#f0fff0;border:1px solid #456047;border-radius:7px;padding:10px;font:inherit}.terminal-compose>div{display:flex;gap:8px}@media(max-width:650px){.terminal-header{display:block}.terminal-header span{display:block}.terminal-shell{font-size:12px}}`; document.head.append(style); })(); diff --git a/scripts/build.mjs b/scripts/build.mjs index c2cfa31..8833789 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -8,7 +8,7 @@ const meta = new URL("../dist/.openai/", import.meta.url); await rm(dist, { recursive: true, force: true }); await Promise.all([mkdir(server, { recursive: true }), mkdir(meta, { recursive: true })]); const [rawIndexHtml, addonsJs, bootGuardJs, i18nJs, tasksJs, calendarJs, i18nRenderersJs, desktopPersonalizationJs, desktopWidgetsJs, browserAppJs, dataActionsJs, terminalAppJs, manifest, serviceWorker, icon, localeEn, localeZh] = await Promise.all([text("index.html"), text("addons.js"), text("boot-guard.js"), text("i18n.js"), text("tasks.js"), text("calendar.js"), text("i18n-renderers.js"), text("desktop-personalization.js"), text("desktop-widgets.js"), text("browser-app.js"), text("data-actions.js"), text("terminal-app.js"), text("manifest.webmanifest"), text("service-worker.js"), text("life-os-icon.svg"), readFile(new URL("../public/locales/en.json", import.meta.url), "utf8"), readFile(new URL("../public/locales/zh-CN.json", import.meta.url), "utf8")]); -const release = "1.11.0"; +const release = "1.12.0"; const localeEnJs = `window.LifeOSLocales=window.LifeOSLocales||{};window.LifeOSLocales.en=${localeEn};`; const localeZhJs = `window.LifeOSLocales=window.LifeOSLocales||{};window.LifeOSLocales['zh-CN']=${localeZh};`; let indexHtml = rawIndexHtml.replace('src="addons.js"', `src="addons.js?v=${release}"`).replace('src="tasks.js"', `src="tasks.js?v=${release}"`).replace('src="calendar.js"', `src="calendar.js?v=${release}"`); diff --git a/scripts/validate.mjs b/scripts/validate.mjs index 1493517..281f167 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -74,8 +74,9 @@ browserNodes['.browser-toolbar'].listeners.click({ target: { closest: () => ({ d assert.equal(browserNodes.iframe.src, 'https://www.google.com/webhp?igu=1'); const terminalStorage = new Map(), terminalRenders = []; const terminalContext = vm.createContext({ - document: { createElement: () => ({ textContent: '' }), head: { append() {} } }, - LifeI18n: { t: key => key }, lang: () => 'en', + document: { createElement: () => ({ textContent: '' }), head: { append() {} }, querySelector: () => null }, + console: { info() {}, warn() {} }, + LifeI18n: { t: key => key, dictionaries: { en: assets.localeEn, zh: assets.localeZh } }, lang: () => 'en', APPS: { calendar: ['icon', 'CALENDAR'] }, openApp: id => terminalRenders.push(id), crypto: globalThis.crypto, day: () => '2026-09-28', mems: () => terminalStorage.get('lifeos_memories') || [], @@ -118,6 +119,7 @@ assert.equal(terminalStorage.get('lifeos_memories').length, 2); const terminalUiNodes = Object.fromEntries(['.terminal-output','.terminal-form','.terminal-compose','.terminal-input','.terminal-note','.terminal-shortcuts'].map(selector => [selector, { value: '', hidden: false, listeners: {}, children: [], focus() {}, addEventListener(type, handler) { this.listeners[type] = handler; }, replaceChildren() { this.children = []; }, append(child) { this.children.push(child); } }])); const terminalUi = { innerHTML: '', dataset: {}, querySelector(selector) { return selector === '.terminal-shell' ? (this.innerHTML ? {} : null) : terminalUiNodes[selector]; } }; terminalContext.terminalApp(terminalUi); +assert.equal(terminalUiNodes['.terminal-output'].children[0].textContent, 'terminal.welcome', 'opening the terminal shows its welcome banner'); const submitTerminal = value => { terminalUiNodes['.terminal-input'].value = value; terminalUiNodes['.terminal-form'].listeners.submit({ preventDefault() {} }); }; submitTerminal('memory add "Via UI" --date 2026-10-01'); for (let index = 0; index < 5; index++) submitTerminal(''); @@ -126,6 +128,14 @@ terminalUiNodes['.terminal-note'].value = 'A note from the guided form'; terminalUiNodes['.terminal-compose'].listeners.click({ target: { closest: () => ({ dataset: { note: 'save' } }) } }); assert.equal(terminalStorage.get('lifeos_memories').find(item => item.date === '2026-10-01').text, 'A note from the guided form'); assert.equal(terminalUiNodes['.terminal-compose'].hidden, true); +submitTerminal('goal add "UI goal"'); +submitTerminal(''); submitTerminal(''); submitTerminal(''); +assert.match(terminalUiNodes['.terminal-output'].children.at(-1).textContent, /terminal\.flow\.confirmCreate/); +submitTerminal('yes'); +assert.equal(terminalStorage.get('lifeos_system').tasksBoard.goals[0].title, 'UI goal'); +submitTerminal('goal delete "UI goal"'); +submitTerminal('yes'); +assert.equal(terminalStorage.get('lifeos_system').tasksBoard.goals.some(item => item.title === 'UI goal'), false); assert.match(command('goal add').message, /terminal\.flow\.title/); assert.match(command('A guided goal').message, /terminal\.flow\.description/); assert.match(command('').message, /terminal\.flow\.targetDate/); @@ -158,6 +168,23 @@ terminalContext.LifeOSActions.createMemory({ date: '2026-10-02', title: 'Linked assert.match(command('task delete 1').message, /terminal\.flow\.confirmDelete/); assert.match(command('yes').message, /terminal\.flow\.deleted/); assert.equal(terminalStorage.get('lifeos_memories').find(item => item.date === '2026-10-02').taskId, ''); +assert.ok(command('date').message.length > 8); +assert.ok(command('time').message.includes(':')); +assert.ok(command('datetime').message.length > 12); +assert.equal(command('uptime').message, 'terminal.uptime'); +assert.equal(command('version').message, 'Life OS v1.12.0'); +assert.equal(command('status').message, 'terminal.status'); +assert.match(command('apps').message, /calendar/); +assert.equal(command('echo "hello Life OS"').message, 'hello Life OS'); +assert.match(terminalContext.LifeOS.help(), /memory add/); +assert.match(terminalContext.LifeOS.run('goal add'), /terminal\.flow\.title/); +assert.equal(terminalContext.LifeOS.cancel(), 'terminal.flow.cancelled'); +assert.match(terminalContext.LifeOS.run('memory add "Console memory" --date 2026-10-03'), /terminal\.wizard\.tags/); +for (let index = 0; index < 5; index++) terminalContext.LifeOS.run(''); +assert.equal(terminalContext.LifeOS.note('Created through DevTools'), 'terminal.memorySaved'); +assert.equal(terminalStorage.get('lifeos_memories').find(item => item.date === '2026-10-03').text, 'Created through DevTools'); +assert.equal(terminalContext.LifeOS.note('Again'), 'terminal.noNote'); +assert.equal(terminalContext.LifeOS.run('window.alert(1)'), 'terminal.unknownCommand'); assert.equal(command('open calendar').message, 'terminal.opened'); assert.ok(terminalRenders.includes('calendar')); assert.deepEqual(Array.from(terminalContext.LifeOSTerminalEngine.words('task add "quoted title" --priority high')), ['task','add','quoted title','--priority','high']);