@hraness/lifecharts 1.0.0
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/LICENSE +21 -0
- package/README.md +75 -0
- package/bin/lifecharts.mjs +30 -0
- package/package.json +39 -0
- package/skills/lifecharts/LICENSE.md +21 -0
- package/skills/lifecharts/SKILL.md +101 -0
- package/skills/lifecharts/agents/openai.yaml +4 -0
- package/skills/lifecharts/references/chart-format.md +76 -0
- package/skills/lifecharts/scripts/lifecharts.mjs +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hraness contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Lifecharts
|
|
2
|
+
|
|
3
|
+
Turn dates and chapters into a life chart you can open, edit, share, and embed at [lifecharts.io](https://lifecharts.io). This package includes a local CLI and the Lifecharts agent skill. Commands create and validate chart links without uploading your data.
|
|
4
|
+
|
|
5
|
+
Use Node.js 22.14 or newer, or Bun 1.3.14 or newer. The package has no runtime dependencies or installation scripts.
|
|
6
|
+
|
|
7
|
+
## Run the CLI
|
|
8
|
+
|
|
9
|
+
The immutable GitHub Release archive is the canonical distribution. Run version 1.0.0 directly:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npx --yes --package=https://github.com/hraness/lifecharts/releases/download/v1.0.0/hraness-lifecharts-1.0.0.tgz lifecharts --help
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
When this version is available on npm, its archive is an exact-byte mirror:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npx --yes @hraness/lifecharts@1.0.0 --help
|
|
19
|
+
npm install --global @hraness/lifecharts@1.0.0
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Write your chapters into `timeline.json`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"name": "Sam",
|
|
27
|
+
"start": "2016-06",
|
|
28
|
+
"view": "bars",
|
|
29
|
+
"chapters": [
|
|
30
|
+
{ "label": "Design school", "start": "2016-06", "end": "2020-05" },
|
|
31
|
+
{ "label": "Independent work", "start": "2020-06", "end": "present" }
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The example is illustrative; use your own facts. A birthday is optional. If you know it, use `birthDate` instead of `start`. Dates can preserve month precision. Chapters can overlap.
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
lifecharts create timeline.json > timeline.url
|
|
40
|
+
lifecharts verify timeline.url
|
|
41
|
+
lifecharts inspect timeline.url > chart.json
|
|
42
|
+
# Edit chart.json, then compile the complete document:
|
|
43
|
+
lifecharts compile chart.json > updated.url
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Without a global installation, use this pinned command prefix before the command and arguments:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npx --yes --package=https://github.com/hraness/lifecharts/releases/download/v1.0.0/hraness-lifecharts-1.0.0.tgz lifecharts create timeline.json --json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`create --json` returns the complete document, share link, edit link, and embed link. Use `-` for stdin. Quote complete URLs in shell commands.
|
|
53
|
+
|
|
54
|
+
## Use with an agent
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
npx skills add https://lifecharts.io --skill lifecharts
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Ask your agent: “Make a life chart from these résumé dates, preserve overlaps, and give me a verified Lifecharts link.” Or provide an existing link and ask for an edit.
|
|
61
|
+
|
|
62
|
+
The same skill is included at `skills/lifecharts/SKILL.md` in this package. From inside the installed package directory, its standalone helper runs with either runtime:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
node skills/lifecharts/scripts/lifecharts.mjs --help
|
|
66
|
+
bun skills/lifecharts/scripts/lifecharts.mjs --help
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Read [the format reference](skills/lifecharts/references/chart-format.md) for limits, date precision, nested chapters, and display options. A LinkedIn profile is context for your agent; this CLI does not fetch profiles or infer missing personal facts.
|
|
70
|
+
|
|
71
|
+
## Sharing and release verification
|
|
72
|
+
|
|
73
|
+
The returned URL contains the chart. Anyone with it can read its title, dates, chapters, notes, and links. Ordinary fragment links keep that data outside the page request. Named social preview images are a separate opt-in action on the website.
|
|
74
|
+
|
|
75
|
+
Releases include the package archive, `release.json`, and `SHA256SUMS`. Verify the archive checksum before installing from a downloaded file. Published GitHub releases are immutable. The npm mirror can follow later without delaying the canonical release.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{createReadStream as ZJ,realpathSync as p}from"node:fs";import{fileURLToPath as $J}from"node:url";var i=Object.freeze([{name:"Ocean",color:"#3478a1"},{name:"Terracotta",color:"#bd684d"},{name:"Forest",color:"#52866b"},{name:"Plum",color:"#876aa4"},{name:"Gold",color:"#ad8c35"},{name:"Rose",color:"#ae6680"},{name:"Slate",color:"#687d8d"},{name:"Teal",color:"#398d8c"}].map((Q)=>Object.freeze(Q))),k=Object.freeze([...i.map(({color:Q})=>Q),"#4863b5","#ad553e","#779446","#a858a0","#c18f4e","#4b9ca3","#745846","#a44f69","#6484bd","#719f7c","#927cb9","#bb785b","#42756a","#97742e","#7f6f93","#be848f","#50688d","#949354","#976466","#668f9a","#a477a1","#7a6841"]);function I(Q){let J=new Map;for(let Z of Q)J.set(Z.toLowerCase(),(J.get(Z.toLowerCase())??0)+1);let V=k[0];if(!V)throw Error("Timeline palette is empty.");for(let Z of k)if((J.get(Z)??0)<(J.get(V)??0))V=Z;return V}var g=16384,w=24576,O="#t=1.",r=86400000,A=new TextEncoder,R=new Set(["__proto__","prototype","constructor"]);function W(Q){throw TypeError(Q)}function T(Q,J,V){if(typeof Q!=="object"||Q===null||Array.isArray(Q))W(`${V} must be an object.`);let Z=Object.getPrototypeOf(Q);if(Z!==Object.prototype&&Z!==null)W(`${V} has an unsupported prototype.`);for(let $ of Reflect.ownKeys(Q)){if(typeof $!=="string"||R.has($)||!J.includes($))W(`${V} contains an unsupported field.`);let Y=Object.getOwnPropertyDescriptor(Q,$);if(!Y||!("value"in Y))W(`${V} cannot contain computed fields.`)}return Q}function P(Q,J,V,Z=!1,$=!1){if(typeof Q!=="string")W(`${V} must contain 1–${J} characters.`);let Y=$?Q.trim():Q;if(Y.trim().length===0||Y.length>J)W(`${V} must contain 1–${J} characters.`);if((Z?/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u:/[\u0000-\u001f\u007f]/u).test(Q)||new TextDecoder().decode(A.encode(Q))!==Q)W(`${V} contains unsupported characters.`);return Y}function t(Q){if(!Array.isArray(Q)||Object.getPrototypeOf(Q)!==Array.prototype||Q.length>30)W("A timeline can contain up to 30 chapters.");if(Reflect.ownKeys(Q).length!==Q.length+1)W("Chapters must be a plain, complete list.");let J=[];for(let V=0;V<Q.length;V++){let Z=Object.getOwnPropertyDescriptor(Q,String(V));if(!Z||!("value"in Z))W("Chapters cannot contain missing or computed entries.");J.push(Z.value)}return J}function M(Q,J){if(typeof Q!=="string"||!/^\d{4}-\d{2}(?:-\d{2})?$/u.test(Q))W(`${J} must be YYYY-MM or YYYY-MM-DD.`);let V=Number(Q.slice(0,4)),Z=Number(Q.slice(5,7)),$=Q.length===7?1:Number(Q.slice(8,10)),Y=new Date(0);if(Y.setUTCFullYear(V,Z-1,$),Y.setUTCHours(0,0,0,0),V<1||Y.getUTCFullYear()!==V||Y.getUTCMonth()+1!==Z||Y.getUTCDate()!==$)W(`${J} must be a real calendar date.`);return{year:V,month:Z,day:$,days:Y.getTime()/r,iso:`${Q.slice(0,7)}-${String($).padStart(2,"0")}`}}function y(Q,J){let V=Q.year+J;if(V>9999)W("The chosen horizon must fall before year 10000.");let Z=V%4===0&&(V%100!==0||V%400===0),$=Q.month===2&&Q.day===29&&!Z?28:Q.day;return M(`${String(V).padStart(4,"0")}-${String(Q.month).padStart(2,"0")}-${String($).padStart(2,"0")}`,"Horizon")}function a(Q){let J=P(Q,500,"Chapter link");if(!/^https?:\/\//iu.test(J)||/[\s\\]/u.test(J))W("Chapter links must be complete HTTP or HTTPS URLs.");let V=new URL(J);if(!["http:","https:"].includes(V.protocol)||V.username||V.password||!V.hostname)W("Chapter links cannot contain credentials or executable schemes.");return J}function f(Q){return[Q.title,Q.start,Q.startKind==="birth"?0:1,Q.asOf,Q.horizonAge,Q.scale==="lived"?0:1,["system","light","dark"].indexOf(Q.theme),Q.chapters.map((J)=>[J.id,J.label,J.start,J.end,J.color,J.description??null,J.url??null,...J.parentId!==void 0?[J.dateLabel??null,J.parentId]:J.dateLabel===void 0?[]:[J.dateLabel]]),...Q.view===void 0?[]:[Q.view==="timeline"?0:1]]}function e(Q){let J=T(Q,["version","title","start","startKind","asOf","horizonAge","scale","theme","view","chapters"],"Timeline");if(J.version!==1)W("This timeline version is not supported.");let V=P(J.title,80,"Timeline title"),Z=M(J.start,"Timeline start");if(J.startKind!=="birth"&&J.startKind!=="timeline")W("Choose a birth date or timeline start.");if(J.asOf!=="live"&&M(J.asOf,"As-of date").days<Z.days)W("The as-of date cannot precede the timeline start.");if(typeof J.horizonAge!=="number"||!Number.isInteger(J.horizonAge)||J.horizonAge<1||J.horizonAge>150)W("Choose a horizon between 1 and 150 years.");if(y(Z,J.horizonAge),J.scale!=="lived"&&J.scale!=="whole")W("Choose a lived or whole timeline scale.");if(J.theme!=="system"&&J.theme!=="light"&&J.theme!=="dark")W("Choose a system, light, or dark theme.");if(J.view!==void 0&&J.view!=="timeline"&&J.view!=="bars")W("Choose the Timeline or Bars view.");let $=new Set,Y=t(J.chapters).map((G)=>{let B=T(G,["id","label","start","end","color","description","url","dateLabel","parentId"],"Chapter"),X=P(B.id,40,"Chapter ID");if(!/^[A-Za-z0-9_-]+$/u.test(X)||R.has(X)||$.has(X))W("Chapter IDs must be safe and unique.");$.add(X);let q=B.parentId===void 0?void 0:P(B.parentId,40,"Parent chapter ID");if(q!==void 0&&(!/^[A-Za-z0-9_-]+$/u.test(q)||R.has(q)||q===X))W("A parent chapter ID must be safe and refer to another chapter.");let D=P(B.label,60,"Chapter label"),z=M(B.start,"Chapter start");if(z.days<Z.days)W("A chapter cannot start before the timeline.");if(B.end!=="present"&&M(B.end,"Chapter end").days<=z.days)W("A chapter must end after it starts.");if(typeof B.color!=="string"||!/^#[a-f0-9]{6}$/iu.test(B.color))W("Choose a six-digit hex chapter color.");return Object.freeze({id:X,label:D,start:B.start,end:B.end,color:B.color,...B.description===void 0?{}:{description:P(B.description,240,"Chapter description",!0)},...B.url===void 0?{}:{url:a(B.url)},...B.dateLabel===void 0?{}:{dateLabel:P(B.dateLabel,80,"Chapter date label",!1,!0)},...q===void 0?{}:{parentId:q}})}),K=new Map(Y.map((G)=>[G.id,G]));for(let G of Y){let B=new Set([G.id]),X=G.parentId;while(X!==void 0){if(B.has(X))W("Chapter nesting cannot contain a cycle.");B.add(X);let q=K.get(X);if(!q)W("A parent chapter must exist in this timeline.");X=q.parentId}}let j=Object.freeze({version:1,title:V,start:J.start,startKind:J.startKind,asOf:J.asOf,horizonAge:J.horizonAge,scale:J.scale,theme:J.theme,...J.view===void 0?{}:{view:J.view},chapters:Object.freeze(Y)});if(A.encode(JSON.stringify(f(j))).byteLength>g)W("This timeline is too large to share. Shorten some descriptions or links.");return j}function U(Q){try{return{ok:!0,value:e(Q)}}catch(J){return{ok:!1,message:J instanceof TypeError?J.message:"This timeline could not be read."}}}function v(Q){let J="";for(let V of Q)J+=String.fromCharCode(V);return btoa(J).replaceAll("+","-").replaceAll("/","_").replace(/=+$/u,"")}function S(Q){let J=U(Q);if(!J.ok)throw TypeError(J.message);let V=O+v(A.encode(JSON.stringify(f(J.value))));if(V.length>w)W("This timeline link is too long.");return V}function _(Q){try{if(typeof Q!=="string"||Q.length>w||!Q.startsWith(O))W("This is not a supported timeline link.");let J=Q.slice(O.length);if(!/^[A-Za-z0-9_-]+$/u.test(J)||J.length%4===1)W("The timeline link is malformed.");let V=atob(J.replaceAll("-","+").replaceAll("_","/"));if(V.length>g)W("This timeline link is too large.");let Z=Uint8Array.from(V,(K)=>K.charCodeAt(0));if(v(Z)!==J)W("The timeline link is malformed.");let $=JSON.parse(new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0}).decode(Z));if(!Array.isArray($)||$.length!==8&&$.length!==9||!Array.isArray($[7])||$[7].length>30)W("The timeline link has an unsupported shape.");if($.length===9&&$[8]!==0&&$[8]!==1)W("The timeline link has an unsupported view.");let Y=$[7].map((K)=>{if(!Array.isArray(K)||K.length!==7&&K.length!==8&&K.length!==9)W("The timeline link contains a malformed chapter.");return{id:K[0],label:K[1],start:K[2],end:K[3],color:K[4],...K[5]===null?{}:{description:K[5]},...K[6]===null?{}:{url:K[6]},...K.length===8||K.length===9&&K[7]!==null?{dateLabel:K[7]}:{},...K.length===9?{parentId:K[8]}:{}}});return U({version:1,title:$[0],start:$[1],startKind:$[2]===0?"birth":$[2]===1?"timeline":null,asOf:$[3],horizonAge:$[4],scale:$[5]===0?"lived":$[5]===1?"whole":null,theme:$[6]===0?"system":$[6]===1?"light":$[6]===2?"dark":null,...$.length===9?{view:$[8]===0?"timeline":"bars"}:{},chapters:Y})}catch(J){return{ok:!1,message:J instanceof TypeError?J.message:"The timeline link is incomplete or malformed."}}}function E(Q,J){return Math.max(0,Math.min(100,Q/Math.max(1,J)*100))}function m(Q,J){let V=U(Q);if(!V.ok)throw TypeError(V.message);let Z=M(Q.start,"Timeline start"),$=M(Q.asOf==="live"?J:Q.asOf,"As-of date"),Y=$.days<Z.days?Z:$,K=y(Z,Q.horizonAge),j=Y.days-Z.days,G=K.days-Z.days,B=Q.scale==="lived"?Y:Y.days>K.days?Y:K,X=Math.max(1,B.days-Z.days),q=Q.chapters.map((D)=>{let z=M(D.start,"Chapter start").days,H=D.end==="present"?Math.max(z,Y.days):M(D.end,"Chapter end").days,b=Math.max(0,Math.min(H,Y.days)-z),x=E(z-Z.days,X),n=E(H-Z.days,X);return Object.freeze({id:D.id,startPercent:x,widthPercent:Math.max(0,n-x),livedPercent:E(b,j),totalPercent:E(b,G),durationDays:H-z})});return Object.freeze({asOf:Y.iso,livedDays:j,totalDays:G,horizonIso:K.iso,extentIso:B.iso,beyondHorizon:Y.days>K.days,denominator:X,segments:Object.freeze(q)})}function JJ(Q){let J=U(Q);if(!J.ok)throw TypeError(J.message);return J.value}var h=JJ({version:1,title:"A life in chapters",start:"1992-04",startKind:"birth",asOf:"2026-09-01",horizonAge:90,scale:"lived",theme:"system",chapters:[{id:"growing-up",label:"Growing up",start:"1992-04",end:"2010-09",color:"#3478a1"},{id:"learning",label:"Learning something new",start:"2010-09",end:"2014-06",color:"#bd684d"},{id:"work",label:"Building a career",start:"2014-06",end:"present",color:"#52866b"},{id:"music",label:"Making music",start:"2018-01",end:"present",color:"#876aa4"}]});var L="https://lifecharts.io",QJ=65536,VJ=new Set(["lifecharts.io","www.lifecharts.io","lifedaysleft.com","www.lifedaysleft.com"]);function d(Q){if(typeof Q!=="string"||Q.length>QJ)return{ok:!1,message:"Paste one chart URL or JSON document under 64 KB."};let J=Q.trim();if(J.startsWith("{"))try{return U(JSON.parse(J))}catch{return{ok:!1,message:"The chart JSON is incomplete or malformed."}}if(J.startsWith("#"))return _(J);try{let V=new URL(J);if(V.protocol!=="https:"||!VJ.has(V.hostname)||V.port||V.username||V.password||!["/","/view","/embed"].includes(V.pathname))return{ok:!1,message:"Use a complete Lifecharts chart URL, its fragment, or lossless chart JSON."};if(V.search){let $=[...V.searchParams];if(V.pathname!=="/view"||V.hash||$.length!==1||$[0]?.[0]!=="chart")return{ok:!1,message:"Use one chart preview parameter without a second fragment or other query fields."};return _(`#${$[0][1]}`)}let Z=_(V.hash);return Z.ok&&V.pathname==="/embed"&&Z.value.view===void 0?U({...Z.value,view:"bars"}):Z}catch{return{ok:!1,message:"Paste a complete Lifecharts chart URL or lossless chart JSON."}}}function s(Q,J,V){if(!Q||typeof Q!=="object"||Array.isArray(Q)||![Object.prototype,null].includes(Object.getPrototypeOf(Q)))throw TypeError(`${V} must be a plain object.`);for(let Z of Reflect.ownKeys(Q))if(typeof Z!=="string"||!J.includes(Z)||!Object.getOwnPropertyDescriptor(Q,Z)?.hasOwnProperty("value"))throw TypeError(`${V} contains an unsupported field.`);return Q}function l(Q){try{if(typeof Q==="object"&&Q!==null&&Object.getOwnPropertyDescriptor(Q,"version")?.value!==void 0)return U(Q);let J=s(Q,["title","name","birthDate","start","asOf","horizonAge","scale","theme","view","chapters"],"Chart");if(J.birthDate!==void 0&&J.start!==void 0)throw TypeError("Choose birthDate or start, not both.");if(J.name!==void 0&&(typeof J.name!=="string"||J.name.trim().length===0||J.name.length>60||/[\u0000-\u001f\u007f]/u.test(J.name)))throw TypeError("Name must contain 1–60 characters.");if(J.chapters!==void 0&&!Array.isArray(J.chapters))throw TypeError("Chapters must be a list.");let V=J.chapters??[];if(!Array.isArray(V)||Object.getPrototypeOf(V)!==Array.prototype||V.length>30||Reflect.ownKeys(V).length!==V.length+1)throw TypeError("A chart can contain up to 30 chapters in a complete list.");let Z=Array.from({length:V.length},(B,X)=>{let q=Object.getOwnPropertyDescriptor(V,String(X));if(!q||!("value"in q))throw TypeError("Chapters must be plain entries without computed fields.");return s(q.value,["id","label","start","end","color","description","url","dateLabel","parentId"],"Chapter")}),$=new Set(Z.flatMap((B)=>typeof B.id==="string"?[B.id]:[])),Y=Z.flatMap((B)=>typeof B.color==="string"?[B.color]:[]),K=Z.map((B,X)=>{let q=`chapter-${X+1}`;while($.has(q))q+="-new";$.add(q);let D=B.color??I(Y);if(typeof D==="string")Y.push(D);return{...B,id:B.id??q,color:D,end:B.end??"present"}}),j=Z.map((B)=>B.start).filter((B)=>typeof B==="string").sort()[0],G=J.birthDate??J.start??j;if(G===void 0)throw TypeError("Add a birthDate, timeline start, or at least one dated chapter.");return U({version:1,title:J.title??(J.name?`${J.name}'s life`:"My life chart"),start:G,startKind:J.birthDate===void 0?"timeline":"birth",asOf:J.asOf??"live",horizonAge:J.horizonAge??90,scale:J.scale??"lived",theme:J.theme??"system",view:J.view??"timeline",chapters:K})}catch(J){return{ok:!1,message:J instanceof TypeError?J.message:"The chart could not be created."}}}function C(Q){let J=S({...Q,view:Q.view??"timeline"}),V=_(J);if(!V.ok||S(V.value)!==J)throw TypeError("The generated chart failed verification.");return{url:`${L}/view${J}`,editUrl:`${L}/${J}`,embedUrl:`${L}/embed${J}`}}function u(Q,J){let V=m(Q,J);return{document:Q,...C(Q),geometry:V,percentageMeaning:Q.scale==="whole"?"Elapsed chapter time relative to the chosen horizon; not a lifespan prediction.":Q.startKind==="birth"?"Elapsed chapter time as a share of life lived since the supplied birthday.":"Elapsed chapter time as a share of time since the chart start; no birthday or lifetime percentage is inferred.",warnings:[...Q.startKind==="birth"?["The horizon is a display choice, not an estimate of lifespan."]:[],...Q.chapters.some((Z)=>Z.start>V.asOf)?["Some chapters begin after the as-of date and are planned, not elapsed."]:[],...Q.chapters.some((Z)=>Z.parentId!==void 0)?["Parent and child chapters can overlap; chapter percentages are not additive."]:[]]}}var c=`Lifecharts — create, edit, and share a life chart locally
|
|
3
|
+
|
|
4
|
+
Usage: lifecharts <command> [input] [--json] [--today YYYY-MM-DD]
|
|
5
|
+
|
|
6
|
+
create <file|-> Create from friendly JSON or a complete chart document
|
|
7
|
+
compile <file|-> Compile a complete, lossless chart JSON document
|
|
8
|
+
edit <file|-> Alias of compile; preserves all supplied IDs and fields
|
|
9
|
+
inspect <URL|file|-> Print lossless chart JSON; --json includes links and geometry
|
|
10
|
+
validate <file|-> Check friendly creation JSON or complete chart JSON
|
|
11
|
+
verify <URL|file|-> Decode, validate, and check the canonical URL round trip
|
|
12
|
+
templates [list|life|career]
|
|
13
|
+
List starters or print editable illustrative JSON
|
|
14
|
+
--help Show this help
|
|
15
|
+
--version Show the portable format and CLI version
|
|
16
|
+
|
|
17
|
+
Create/compile print a verified share URL; --json also returns edit and embed URLs.
|
|
18
|
+
Input '-' reads stdin. A file can contain one URL, fragment, or JSON document.
|
|
19
|
+
The --today date fixes inspection geometry; it does not change the document.
|
|
20
|
+
No command sends a request or fetches a profile. Quote complete URLs in a shell.
|
|
21
|
+
Keep the returned URL intact. Anyone with it can read the chart; it is not encrypted.
|
|
22
|
+
|
|
23
|
+
Create input: {"name":"Sam","birthDate":"1990-04-12","view":"bars","chapters":[]}
|
|
24
|
+
Birthday is optional. Supply start or a dated chapter when birthDate is unknown.
|
|
25
|
+
For an edit: inspect the URL > chart.json; edit JSON; compile chart.json; verify URL.
|
|
26
|
+
`,BJ=new Set(["create","compile","edit","inspect","validate","verify","templates"]),N=(Q,J=1)=>({ok:!1,stderr:`${Q}
|
|
27
|
+
`,exitCode:J}),F=(Q)=>({ok:!0,stdout:`${typeof Q==="string"?Q:JSON.stringify(Q,null,2)}
|
|
28
|
+
`,exitCode:0});async function YJ(Q,J){let V=Q==="-"?await J.readStdin():Q.startsWith("https://")||Q.startsWith("#")?Q:await J.readFile(Q);if(new TextEncoder().encode(V).byteLength>65536)throw TypeError("Input must be under 64 KB.");return V}async function qJ(Q,J){try{if(!Array.isArray(Q)||Q.some((q)=>typeof q!=="string"))return N("Arguments must be text.",2);if(Q.length===0||Q.length===1&&["--help","help","-h"].includes(Q[0]??""))return F(c.trimEnd());if(Q.length===1&&Q[0]==="--version")return F("Lifecharts CLI 1.0.0 · timeline format 1");let V=Q[0]??"";if(!BJ.has(V))return N("Unknown command. Run --help for available commands.",2);let Z=[],$=!1,Y,K=!1;for(let q=1;q<Q.length;q++){let D=Q[q]??"";if(D==="--json"&&!$)$=!0;else if(D==="--help"&&!K)K=!0;else if(D==="--today"&&Y===void 0){if(Y=Q[++q],!Y||!/^\d{4}-\d{2}-\d{2}$/u.test(Y))return N("--today needs a YYYY-MM-DD date.",2)}else if(D.startsWith("-")&&D!=="-")return N("Unknown or repeated option. Run --help for available options.",2);else Z.push(D)}if(K)return Z.length===0&&!$&&Y===void 0?F(c.trimEnd()):N("Use <command> --help by itself.",2);if(V==="templates"){if(Z.length>1||Y!==void 0)return N("Use templates [list|life|career].",2);let q=Z[0]??"list";if(q==="list")return F($?[{name:"life",description:"Illustrative life with overlapping chapters"},{name:"career",description:"Illustrative career without an inferred birthday"}]:`life Illustrative life with overlapping chapters
|
|
29
|
+
career Illustrative career without an inferred birthday`);if(q==="life")return F(h);if(q==="career")return F({version:1,title:"An example career",start:"2016-06",startKind:"timeline",asOf:"2026-09-01",horizonAge:40,scale:"lived",theme:"system",view:"bars",chapters:[{id:"first-role",label:"First role",start:"2016-06",end:"2020-03",color:"#3478a1"},{id:"next-role",label:"A new direction",start:"2020-03",end:"present",color:"#bd684d"},{id:"side-project",label:"Side project",start:"2023-01",end:"present",color:"#52866b"}]});return N("Unknown template. Run templates list.",2)}if(Z.length!==1)return N("Supply one input file, URL, or '-' for stdin. Run --help for command details.",2);if(Y!==void 0&&!["inspect","verify"].includes(V))return N("--today is supported by inspect and verify only.",2);let j=await YJ(Z[0]??"",J),G;if(["create","compile","edit","validate"].includes(V)){let q;try{q=JSON.parse(j)}catch{return N("The input must be one complete JSON object.")}G=V==="compile"||V==="edit"?U(q):l(q)}else G=d(j);if(!G.ok)return N(G.message);let B=["create","compile","edit"].includes(V)?{...G.value,view:G.value.view??"timeline"}:G.value;if(V==="inspect"&&!$&&Y===void 0)return F(B);if(V==="validate")return F($?{valid:!0,document:B}:`Valid chart: ${B.title} (${B.chapters.length} chapters).`);if(V==="inspect"||V==="verify"){let q=u(B,Y??J.today());return F($?{verified:!0,...q}:V==="inspect"?q:`Verified chart: ${B.title}
|
|
30
|
+
${q.url}`)}let X=C(B);return F($?{verified:!0,document:B,...X}:X.url)}catch(V){return N(V instanceof TypeError?V.message:"The input could not be read. Check the file path, permissions, and JSON.")}}async function o(Q){let J=[],V=0;for await(let Y of Q){if(V+=Y.byteLength,V>65536)throw TypeError("Input must be under 64 KB.");J.push(Y)}let Z=new Uint8Array(V),$=0;for(let Y of J)Z.set(Y,$),$+=Y.byteLength;try{return new TextDecoder("utf-8",{fatal:!0}).decode(Z)}catch{throw TypeError("Input must be valid UTF-8 text.")}}function WJ(){try{return process.argv[1]!==void 0&&p(process.argv[1])===p($J(import.meta.url))}catch{return!1}}if(WJ()){let Q=await qJ(process.argv.slice(2),{readFile:(J)=>o(ZJ(J,{highWaterMark:65537})),readStdin:()=>o(process.stdin),today:()=>new Date().toISOString().slice(0,10)});if(Q.ok)process.stdout.write(Q.stdout);else process.stderr.write(Q.stderr);process.exitCode=Q.exitCode}export{qJ as runLifechartsCli,c as LIFECHARTS_HELP};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hraness/lifecharts",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Create, edit, and verify portable Lifecharts timelines locally, with an agent skill included.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.14.0"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"lifecharts": "bin/lifecharts.mjs"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/lifecharts.mjs",
|
|
15
|
+
"skills/lifecharts",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/hraness/lifecharts.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://lifecharts.io/agents",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/hraness/lifecharts/issues"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"lifecharts",
|
|
29
|
+
"timeline",
|
|
30
|
+
"life",
|
|
31
|
+
"career",
|
|
32
|
+
"agent-skill",
|
|
33
|
+
"cli"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"registry": "https://registry.npmjs.org/"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hraness contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lifecharts
|
|
3
|
+
description: Create, inspect, and edit personal life timelines and career charts on Lifecharts from user-supplied facts, dates, résumés, or authorized profile material; return a verified chart link and lossless JSON.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lifecharts
|
|
7
|
+
|
|
8
|
+
Lifecharts makes a life timeline whose document travels in its URL. Use the
|
|
9
|
+
bundled checked CLI to create, inspect, edit, validate, and share it. No account,
|
|
10
|
+
API key, or source checkout is required. Run it with Node.js 22.14 or newer:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
node <skill-path>/scripts/lifecharts.mjs --help
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Bun 1.3.14 or newer also works; replace `node` with `bun`. The
|
|
17
|
+
`@hraness/lifecharts` package includes this skill and exposes the same commands
|
|
18
|
+
through its `lifecharts` executable. Use the bundled script when the skill is
|
|
19
|
+
already installed; no separate package installation is needed.
|
|
20
|
+
|
|
21
|
+
## Create a useful chart
|
|
22
|
+
|
|
23
|
+
Start with the facts the user supplies: chapters, dates, a résumé, or authorized
|
|
24
|
+
profile material. A birthday and LinkedIn profile are optional. If a profile
|
|
25
|
+
is inaccessible, ask for the relevant pasted text. The CLI does not fetch URLs.
|
|
26
|
+
Do not infer birthdays from graduation years, invent personal milestones, or
|
|
27
|
+
turn vague years into precise dates. Ask for the missing month when it matters;
|
|
28
|
+
preserve supplied month-only dates as `YYYY-MM`.
|
|
29
|
+
|
|
30
|
+
Write a small JSON file with a title or name and chapters. Use `birthDate` only
|
|
31
|
+
when supplied; otherwise use `start` or let the earliest chapter establish the
|
|
32
|
+
timeline start. Chapter IDs and colors are generated when omitted. Add explicit
|
|
33
|
+
IDs when authoring parent/child relationships. Use `end: "present"` for ongoing
|
|
34
|
+
chapters; future dates represent plans, not facts that have already happened.
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"name": "Sam",
|
|
39
|
+
"start": "2016-06",
|
|
40
|
+
"view": "bars",
|
|
41
|
+
"chapters": [
|
|
42
|
+
{ "label": "Design school", "start": "2016-06", "end": "2020-05" },
|
|
43
|
+
{ "label": "Independent work", "start": "2020-06", "end": "present" }
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
That example is illustrative. Replace it with the user's facts before delivery.
|
|
49
|
+
`templates list`, `templates life`, and `templates career` supply other editable
|
|
50
|
+
examples. Read [references/chart-format.md](references/chart-format.md) for
|
|
51
|
+
field limits, nesting, display choices, and exact editing.
|
|
52
|
+
|
|
53
|
+
`view: "timeline"` aligns chapters by date and makes overlap visible;
|
|
54
|
+
`view: "bars"` emphasizes each chapter's share of the elapsed timeline.
|
|
55
|
+
Choose the view that suits the story. Preserve intentional overlap and nesting;
|
|
56
|
+
chapter percentages need not add to 100. The default `scale: "lived"` shows
|
|
57
|
+
elapsed time. A whole-life horizon is a display setting the user chooses, never
|
|
58
|
+
a prediction or individualized estimate of lifespan. Without a birthday, don't
|
|
59
|
+
describe chapter shares as percentages of the person's whole life.
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
node <skill-path>/scripts/lifecharts.mjs create input.json --json > created.json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The result contains the canonical `document`, a verified `url`, `editUrl`, and
|
|
66
|
+
`embedUrl`. Save the document as the editable source and the complete URL in
|
|
67
|
+
`final.url`. No create or edit command uploads anything.
|
|
68
|
+
|
|
69
|
+
## Edit without losing information
|
|
70
|
+
|
|
71
|
+
Inspect the complete original URL or a file containing it:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
node <skill-path>/scripts/lifecharts.mjs inspect original.url > chart.json
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Edit that lossless JSON and preserve IDs, ordering, parent IDs, date precision,
|
|
78
|
+
descriptions, links, theme, and unrelated chapters. Compile the edited document:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
node <skill-path>/scripts/lifecharts.mjs compile chart.json > final.url
|
|
82
|
+
node <skill-path>/scripts/lifecharts.mjs verify final.url --json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Read the verification output and check the requested edit and preserved details.
|
|
86
|
+
Never construct or repair the encoded fragment manually. A file can contain one
|
|
87
|
+
URL, fragment, or JSON document; use `-` to read stdin. Quote URLs in shell
|
|
88
|
+
commands. Failures return nonzero with an actionable message.
|
|
89
|
+
|
|
90
|
+
## Deliver
|
|
91
|
+
|
|
92
|
+
Return the complete verified `https://lifecharts.io/view#t=1.…` link with a short
|
|
93
|
+
description of the result. Save lossless JSON so the user can keep editing or
|
|
94
|
+
paste it into Lifecharts. Use the returned `embedUrl` when they request an embed;
|
|
95
|
+
the view, scale, and theme travel with the link.
|
|
96
|
+
|
|
97
|
+
The URL contains the supplied chart data. Anyone receiving it can read and pass
|
|
98
|
+
on those details. Ordinary web requests do not send its fragment to Lifecharts;
|
|
99
|
+
pasting it into a hosted agent sends it to that provider. Personal social-card
|
|
100
|
+
previews are a separate opt-in share action in the website. Keep the ordinary
|
|
101
|
+
fragment link unless the user asks to publish a preview.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Chart format and CLI reference
|
|
2
|
+
|
|
3
|
+
The bundled CLI runs offline under Node.js 22.14 or newer, or Bun 1.3.14 or newer.
|
|
4
|
+
Run `node <skill-path>/scripts/lifecharts.mjs --help` to list all commands. If the
|
|
5
|
+
`@hraness/lifecharts` package is installed, `lifecharts` exposes the same interface.
|
|
6
|
+
Success exits 0, invalid input exits 1, and invalid command syntax exits 2. The CLI
|
|
7
|
+
writes results to stdout and errors to stderr; it never logs the source input.
|
|
8
|
+
|
|
9
|
+
## Creation JSON
|
|
10
|
+
|
|
11
|
+
`create` and `validate` accept friendly creation JSON or a complete version 1
|
|
12
|
+
document. Unknown fields are rejected, including profile URLs: a profile is
|
|
13
|
+
source material for the agent, not a document field.
|
|
14
|
+
|
|
15
|
+
| Field | Meaning |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `title` / `name` | Optional chart title, up to 80 characters, or name up to 60 characters. Title wins when both exist. |
|
|
18
|
+
| `birthDate` | Optional supplied birthday, `YYYY-MM-DD` or `YYYY-MM`. Mutually exclusive with `start`. |
|
|
19
|
+
| `start` | Optional timeline start with the same date precision. Otherwise uses the earliest supplied chapter. |
|
|
20
|
+
| `asOf` | `"live"` (default), or a fixed `YYYY-MM-DD` / `YYYY-MM` date. |
|
|
21
|
+
| `horizonAge` | Integer 1–150, default 90. Years from the start; this is a display horizon. |
|
|
22
|
+
| `scale` | `"lived"` (default) or `"whole"`. |
|
|
23
|
+
| `theme` | `"system"` (default), `"light"`, or `"dark"`. |
|
|
24
|
+
| `view` | `"timeline"` (default) or `"bars"`. |
|
|
25
|
+
| `chapters` | Up to 30 chapters; defaults to empty when a start or birthday is supplied. |
|
|
26
|
+
|
|
27
|
+
A chapter requires `label` (1–60 characters) and `start`. Its `end` defaults to
|
|
28
|
+
`"present"`. Optional `id` is a unique 1–40-character ASCII letter, digit,
|
|
29
|
+
underscore, or hyphen identifier. Optional `color` is a six-digit hex color.
|
|
30
|
+
Missing IDs and colors are generated deterministically without replacing explicit
|
|
31
|
+
values. Dates must be real calendar dates. Explicit ends must follow starts.
|
|
32
|
+
Chapters cannot start before the chart.
|
|
33
|
+
|
|
34
|
+
Additional chapter fields: `description` (up to 240 characters), `url` (complete
|
|
35
|
+
HTTP(S), up to 500 characters, no credentials), `dateLabel` (display-only text,
|
|
36
|
+
up to 80 characters), and `parentId` (an existing chapter ID). Nesting cannot
|
|
37
|
+
contain cycles. Author parents before children when practical; the parser accepts
|
|
38
|
+
any order. Overlap is valid and never implies nesting by itself.
|
|
39
|
+
|
|
40
|
+
## Lossless version 1 JSON
|
|
41
|
+
|
|
42
|
+
`inspect` prints a complete document. `compile` and `edit` accept only that full
|
|
43
|
+
shape: `version: 1`, `title`, `start`, `startKind: "birth" | "timeline"`, `asOf`,
|
|
44
|
+
`horizonAge`, `scale`, `theme`, optional `view`, and `chapters` with explicit IDs
|
|
45
|
+
and colors. Do not use `birthDate` or `name` in a complete document.
|
|
46
|
+
|
|
47
|
+
The codec preserves month precision and optional metadata. Its canonical payload
|
|
48
|
+
must fit in 16 KB; reduce long descriptions or links if compilation reports that
|
|
49
|
+
the chart is too large. JSON edits are the full-document edit interface. There is
|
|
50
|
+
no patch language to silently lose unrelated fields.
|
|
51
|
+
|
|
52
|
+
Older URLs may omit `view`: the original main view used timeline while the
|
|
53
|
+
original embed used bars. Importing an old `/embed` URL records `view: "bars"`
|
|
54
|
+
to preserve its appearance. New links always record a view explicitly; complete
|
|
55
|
+
JSON without a view compiles as `"timeline"` in both viewer and embed.
|
|
56
|
+
|
|
57
|
+
`inspect --json` returns the document, share/edit/embed links, geometry, and
|
|
58
|
+
interpretation notes. `verify --json` additionally reports `verified: true`.
|
|
59
|
+
Use `--today YYYY-MM-DD` with those commands to inspect live geometry at a fixed
|
|
60
|
+
date without changing `asOf` in the document. `validate --json` returns a checked
|
|
61
|
+
document without generating links. `create --json` and `compile --json` return
|
|
62
|
+
the checked document and all three fragment URLs.
|
|
63
|
+
|
|
64
|
+
## Percentages and privacy
|
|
65
|
+
|
|
66
|
+
The lived share is elapsed chapter days divided by elapsed timeline days. The
|
|
67
|
+
whole share is elapsed chapter days divided by the chosen horizon's days.
|
|
68
|
+
Overlapping, parent, and child chapters are not additive. Future parts do not
|
|
69
|
+
count as elapsed. A birth-based chart may show progress toward the chosen horizon;
|
|
70
|
+
that progress is not a mortality estimate.
|
|
71
|
+
|
|
72
|
+
The fragment is encoded, not encrypted, and includes every document field. It is
|
|
73
|
+
not sent in an ordinary request to the website. The selected view, theme, and
|
|
74
|
+
scale apply to both the main chart and the embed. A personal social preview has
|
|
75
|
+
different disclosure semantics and is created explicitly in the website's share
|
|
76
|
+
controls. The CLI's normal output always uses a fragment URL.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{createReadStream as ZJ,realpathSync as p}from"node:fs";import{fileURLToPath as $J}from"node:url";var i=Object.freeze([{name:"Ocean",color:"#3478a1"},{name:"Terracotta",color:"#bd684d"},{name:"Forest",color:"#52866b"},{name:"Plum",color:"#876aa4"},{name:"Gold",color:"#ad8c35"},{name:"Rose",color:"#ae6680"},{name:"Slate",color:"#687d8d"},{name:"Teal",color:"#398d8c"}].map((Q)=>Object.freeze(Q))),k=Object.freeze([...i.map(({color:Q})=>Q),"#4863b5","#ad553e","#779446","#a858a0","#c18f4e","#4b9ca3","#745846","#a44f69","#6484bd","#719f7c","#927cb9","#bb785b","#42756a","#97742e","#7f6f93","#be848f","#50688d","#949354","#976466","#668f9a","#a477a1","#7a6841"]);function I(Q){let J=new Map;for(let Z of Q)J.set(Z.toLowerCase(),(J.get(Z.toLowerCase())??0)+1);let V=k[0];if(!V)throw Error("Timeline palette is empty.");for(let Z of k)if((J.get(Z)??0)<(J.get(V)??0))V=Z;return V}var g=16384,w=24576,O="#t=1.",r=86400000,A=new TextEncoder,R=new Set(["__proto__","prototype","constructor"]);function W(Q){throw TypeError(Q)}function T(Q,J,V){if(typeof Q!=="object"||Q===null||Array.isArray(Q))W(`${V} must be an object.`);let Z=Object.getPrototypeOf(Q);if(Z!==Object.prototype&&Z!==null)W(`${V} has an unsupported prototype.`);for(let $ of Reflect.ownKeys(Q)){if(typeof $!=="string"||R.has($)||!J.includes($))W(`${V} contains an unsupported field.`);let Y=Object.getOwnPropertyDescriptor(Q,$);if(!Y||!("value"in Y))W(`${V} cannot contain computed fields.`)}return Q}function P(Q,J,V,Z=!1,$=!1){if(typeof Q!=="string")W(`${V} must contain 1–${J} characters.`);let Y=$?Q.trim():Q;if(Y.trim().length===0||Y.length>J)W(`${V} must contain 1–${J} characters.`);if((Z?/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u:/[\u0000-\u001f\u007f]/u).test(Q)||new TextDecoder().decode(A.encode(Q))!==Q)W(`${V} contains unsupported characters.`);return Y}function t(Q){if(!Array.isArray(Q)||Object.getPrototypeOf(Q)!==Array.prototype||Q.length>30)W("A timeline can contain up to 30 chapters.");if(Reflect.ownKeys(Q).length!==Q.length+1)W("Chapters must be a plain, complete list.");let J=[];for(let V=0;V<Q.length;V++){let Z=Object.getOwnPropertyDescriptor(Q,String(V));if(!Z||!("value"in Z))W("Chapters cannot contain missing or computed entries.");J.push(Z.value)}return J}function M(Q,J){if(typeof Q!=="string"||!/^\d{4}-\d{2}(?:-\d{2})?$/u.test(Q))W(`${J} must be YYYY-MM or YYYY-MM-DD.`);let V=Number(Q.slice(0,4)),Z=Number(Q.slice(5,7)),$=Q.length===7?1:Number(Q.slice(8,10)),Y=new Date(0);if(Y.setUTCFullYear(V,Z-1,$),Y.setUTCHours(0,0,0,0),V<1||Y.getUTCFullYear()!==V||Y.getUTCMonth()+1!==Z||Y.getUTCDate()!==$)W(`${J} must be a real calendar date.`);return{year:V,month:Z,day:$,days:Y.getTime()/r,iso:`${Q.slice(0,7)}-${String($).padStart(2,"0")}`}}function y(Q,J){let V=Q.year+J;if(V>9999)W("The chosen horizon must fall before year 10000.");let Z=V%4===0&&(V%100!==0||V%400===0),$=Q.month===2&&Q.day===29&&!Z?28:Q.day;return M(`${String(V).padStart(4,"0")}-${String(Q.month).padStart(2,"0")}-${String($).padStart(2,"0")}`,"Horizon")}function a(Q){let J=P(Q,500,"Chapter link");if(!/^https?:\/\//iu.test(J)||/[\s\\]/u.test(J))W("Chapter links must be complete HTTP or HTTPS URLs.");let V=new URL(J);if(!["http:","https:"].includes(V.protocol)||V.username||V.password||!V.hostname)W("Chapter links cannot contain credentials or executable schemes.");return J}function f(Q){return[Q.title,Q.start,Q.startKind==="birth"?0:1,Q.asOf,Q.horizonAge,Q.scale==="lived"?0:1,["system","light","dark"].indexOf(Q.theme),Q.chapters.map((J)=>[J.id,J.label,J.start,J.end,J.color,J.description??null,J.url??null,...J.parentId!==void 0?[J.dateLabel??null,J.parentId]:J.dateLabel===void 0?[]:[J.dateLabel]]),...Q.view===void 0?[]:[Q.view==="timeline"?0:1]]}function e(Q){let J=T(Q,["version","title","start","startKind","asOf","horizonAge","scale","theme","view","chapters"],"Timeline");if(J.version!==1)W("This timeline version is not supported.");let V=P(J.title,80,"Timeline title"),Z=M(J.start,"Timeline start");if(J.startKind!=="birth"&&J.startKind!=="timeline")W("Choose a birth date or timeline start.");if(J.asOf!=="live"&&M(J.asOf,"As-of date").days<Z.days)W("The as-of date cannot precede the timeline start.");if(typeof J.horizonAge!=="number"||!Number.isInteger(J.horizonAge)||J.horizonAge<1||J.horizonAge>150)W("Choose a horizon between 1 and 150 years.");if(y(Z,J.horizonAge),J.scale!=="lived"&&J.scale!=="whole")W("Choose a lived or whole timeline scale.");if(J.theme!=="system"&&J.theme!=="light"&&J.theme!=="dark")W("Choose a system, light, or dark theme.");if(J.view!==void 0&&J.view!=="timeline"&&J.view!=="bars")W("Choose the Timeline or Bars view.");let $=new Set,Y=t(J.chapters).map((G)=>{let B=T(G,["id","label","start","end","color","description","url","dateLabel","parentId"],"Chapter"),X=P(B.id,40,"Chapter ID");if(!/^[A-Za-z0-9_-]+$/u.test(X)||R.has(X)||$.has(X))W("Chapter IDs must be safe and unique.");$.add(X);let q=B.parentId===void 0?void 0:P(B.parentId,40,"Parent chapter ID");if(q!==void 0&&(!/^[A-Za-z0-9_-]+$/u.test(q)||R.has(q)||q===X))W("A parent chapter ID must be safe and refer to another chapter.");let D=P(B.label,60,"Chapter label"),z=M(B.start,"Chapter start");if(z.days<Z.days)W("A chapter cannot start before the timeline.");if(B.end!=="present"&&M(B.end,"Chapter end").days<=z.days)W("A chapter must end after it starts.");if(typeof B.color!=="string"||!/^#[a-f0-9]{6}$/iu.test(B.color))W("Choose a six-digit hex chapter color.");return Object.freeze({id:X,label:D,start:B.start,end:B.end,color:B.color,...B.description===void 0?{}:{description:P(B.description,240,"Chapter description",!0)},...B.url===void 0?{}:{url:a(B.url)},...B.dateLabel===void 0?{}:{dateLabel:P(B.dateLabel,80,"Chapter date label",!1,!0)},...q===void 0?{}:{parentId:q}})}),K=new Map(Y.map((G)=>[G.id,G]));for(let G of Y){let B=new Set([G.id]),X=G.parentId;while(X!==void 0){if(B.has(X))W("Chapter nesting cannot contain a cycle.");B.add(X);let q=K.get(X);if(!q)W("A parent chapter must exist in this timeline.");X=q.parentId}}let j=Object.freeze({version:1,title:V,start:J.start,startKind:J.startKind,asOf:J.asOf,horizonAge:J.horizonAge,scale:J.scale,theme:J.theme,...J.view===void 0?{}:{view:J.view},chapters:Object.freeze(Y)});if(A.encode(JSON.stringify(f(j))).byteLength>g)W("This timeline is too large to share. Shorten some descriptions or links.");return j}function U(Q){try{return{ok:!0,value:e(Q)}}catch(J){return{ok:!1,message:J instanceof TypeError?J.message:"This timeline could not be read."}}}function v(Q){let J="";for(let V of Q)J+=String.fromCharCode(V);return btoa(J).replaceAll("+","-").replaceAll("/","_").replace(/=+$/u,"")}function S(Q){let J=U(Q);if(!J.ok)throw TypeError(J.message);let V=O+v(A.encode(JSON.stringify(f(J.value))));if(V.length>w)W("This timeline link is too long.");return V}function _(Q){try{if(typeof Q!=="string"||Q.length>w||!Q.startsWith(O))W("This is not a supported timeline link.");let J=Q.slice(O.length);if(!/^[A-Za-z0-9_-]+$/u.test(J)||J.length%4===1)W("The timeline link is malformed.");let V=atob(J.replaceAll("-","+").replaceAll("_","/"));if(V.length>g)W("This timeline link is too large.");let Z=Uint8Array.from(V,(K)=>K.charCodeAt(0));if(v(Z)!==J)W("The timeline link is malformed.");let $=JSON.parse(new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0}).decode(Z));if(!Array.isArray($)||$.length!==8&&$.length!==9||!Array.isArray($[7])||$[7].length>30)W("The timeline link has an unsupported shape.");if($.length===9&&$[8]!==0&&$[8]!==1)W("The timeline link has an unsupported view.");let Y=$[7].map((K)=>{if(!Array.isArray(K)||K.length!==7&&K.length!==8&&K.length!==9)W("The timeline link contains a malformed chapter.");return{id:K[0],label:K[1],start:K[2],end:K[3],color:K[4],...K[5]===null?{}:{description:K[5]},...K[6]===null?{}:{url:K[6]},...K.length===8||K.length===9&&K[7]!==null?{dateLabel:K[7]}:{},...K.length===9?{parentId:K[8]}:{}}});return U({version:1,title:$[0],start:$[1],startKind:$[2]===0?"birth":$[2]===1?"timeline":null,asOf:$[3],horizonAge:$[4],scale:$[5]===0?"lived":$[5]===1?"whole":null,theme:$[6]===0?"system":$[6]===1?"light":$[6]===2?"dark":null,...$.length===9?{view:$[8]===0?"timeline":"bars"}:{},chapters:Y})}catch(J){return{ok:!1,message:J instanceof TypeError?J.message:"The timeline link is incomplete or malformed."}}}function E(Q,J){return Math.max(0,Math.min(100,Q/Math.max(1,J)*100))}function m(Q,J){let V=U(Q);if(!V.ok)throw TypeError(V.message);let Z=M(Q.start,"Timeline start"),$=M(Q.asOf==="live"?J:Q.asOf,"As-of date"),Y=$.days<Z.days?Z:$,K=y(Z,Q.horizonAge),j=Y.days-Z.days,G=K.days-Z.days,B=Q.scale==="lived"?Y:Y.days>K.days?Y:K,X=Math.max(1,B.days-Z.days),q=Q.chapters.map((D)=>{let z=M(D.start,"Chapter start").days,H=D.end==="present"?Math.max(z,Y.days):M(D.end,"Chapter end").days,b=Math.max(0,Math.min(H,Y.days)-z),x=E(z-Z.days,X),n=E(H-Z.days,X);return Object.freeze({id:D.id,startPercent:x,widthPercent:Math.max(0,n-x),livedPercent:E(b,j),totalPercent:E(b,G),durationDays:H-z})});return Object.freeze({asOf:Y.iso,livedDays:j,totalDays:G,horizonIso:K.iso,extentIso:B.iso,beyondHorizon:Y.days>K.days,denominator:X,segments:Object.freeze(q)})}function JJ(Q){let J=U(Q);if(!J.ok)throw TypeError(J.message);return J.value}var h=JJ({version:1,title:"A life in chapters",start:"1992-04",startKind:"birth",asOf:"2026-09-01",horizonAge:90,scale:"lived",theme:"system",chapters:[{id:"growing-up",label:"Growing up",start:"1992-04",end:"2010-09",color:"#3478a1"},{id:"learning",label:"Learning something new",start:"2010-09",end:"2014-06",color:"#bd684d"},{id:"work",label:"Building a career",start:"2014-06",end:"present",color:"#52866b"},{id:"music",label:"Making music",start:"2018-01",end:"present",color:"#876aa4"}]});var L="https://lifecharts.io",QJ=65536,VJ=new Set(["lifecharts.io","www.lifecharts.io","lifedaysleft.com","www.lifedaysleft.com"]);function d(Q){if(typeof Q!=="string"||Q.length>QJ)return{ok:!1,message:"Paste one chart URL or JSON document under 64 KB."};let J=Q.trim();if(J.startsWith("{"))try{return U(JSON.parse(J))}catch{return{ok:!1,message:"The chart JSON is incomplete or malformed."}}if(J.startsWith("#"))return _(J);try{let V=new URL(J);if(V.protocol!=="https:"||!VJ.has(V.hostname)||V.port||V.username||V.password||!["/","/view","/embed"].includes(V.pathname))return{ok:!1,message:"Use a complete Lifecharts chart URL, its fragment, or lossless chart JSON."};if(V.search){let $=[...V.searchParams];if(V.pathname!=="/view"||V.hash||$.length!==1||$[0]?.[0]!=="chart")return{ok:!1,message:"Use one chart preview parameter without a second fragment or other query fields."};return _(`#${$[0][1]}`)}let Z=_(V.hash);return Z.ok&&V.pathname==="/embed"&&Z.value.view===void 0?U({...Z.value,view:"bars"}):Z}catch{return{ok:!1,message:"Paste a complete Lifecharts chart URL or lossless chart JSON."}}}function s(Q,J,V){if(!Q||typeof Q!=="object"||Array.isArray(Q)||![Object.prototype,null].includes(Object.getPrototypeOf(Q)))throw TypeError(`${V} must be a plain object.`);for(let Z of Reflect.ownKeys(Q))if(typeof Z!=="string"||!J.includes(Z)||!Object.getOwnPropertyDescriptor(Q,Z)?.hasOwnProperty("value"))throw TypeError(`${V} contains an unsupported field.`);return Q}function l(Q){try{if(typeof Q==="object"&&Q!==null&&Object.getOwnPropertyDescriptor(Q,"version")?.value!==void 0)return U(Q);let J=s(Q,["title","name","birthDate","start","asOf","horizonAge","scale","theme","view","chapters"],"Chart");if(J.birthDate!==void 0&&J.start!==void 0)throw TypeError("Choose birthDate or start, not both.");if(J.name!==void 0&&(typeof J.name!=="string"||J.name.trim().length===0||J.name.length>60||/[\u0000-\u001f\u007f]/u.test(J.name)))throw TypeError("Name must contain 1–60 characters.");if(J.chapters!==void 0&&!Array.isArray(J.chapters))throw TypeError("Chapters must be a list.");let V=J.chapters??[];if(!Array.isArray(V)||Object.getPrototypeOf(V)!==Array.prototype||V.length>30||Reflect.ownKeys(V).length!==V.length+1)throw TypeError("A chart can contain up to 30 chapters in a complete list.");let Z=Array.from({length:V.length},(B,X)=>{let q=Object.getOwnPropertyDescriptor(V,String(X));if(!q||!("value"in q))throw TypeError("Chapters must be plain entries without computed fields.");return s(q.value,["id","label","start","end","color","description","url","dateLabel","parentId"],"Chapter")}),$=new Set(Z.flatMap((B)=>typeof B.id==="string"?[B.id]:[])),Y=Z.flatMap((B)=>typeof B.color==="string"?[B.color]:[]),K=Z.map((B,X)=>{let q=`chapter-${X+1}`;while($.has(q))q+="-new";$.add(q);let D=B.color??I(Y);if(typeof D==="string")Y.push(D);return{...B,id:B.id??q,color:D,end:B.end??"present"}}),j=Z.map((B)=>B.start).filter((B)=>typeof B==="string").sort()[0],G=J.birthDate??J.start??j;if(G===void 0)throw TypeError("Add a birthDate, timeline start, or at least one dated chapter.");return U({version:1,title:J.title??(J.name?`${J.name}'s life`:"My life chart"),start:G,startKind:J.birthDate===void 0?"timeline":"birth",asOf:J.asOf??"live",horizonAge:J.horizonAge??90,scale:J.scale??"lived",theme:J.theme??"system",view:J.view??"timeline",chapters:K})}catch(J){return{ok:!1,message:J instanceof TypeError?J.message:"The chart could not be created."}}}function C(Q){let J=S({...Q,view:Q.view??"timeline"}),V=_(J);if(!V.ok||S(V.value)!==J)throw TypeError("The generated chart failed verification.");return{url:`${L}/view${J}`,editUrl:`${L}/${J}`,embedUrl:`${L}/embed${J}`}}function u(Q,J){let V=m(Q,J);return{document:Q,...C(Q),geometry:V,percentageMeaning:Q.scale==="whole"?"Elapsed chapter time relative to the chosen horizon; not a lifespan prediction.":Q.startKind==="birth"?"Elapsed chapter time as a share of life lived since the supplied birthday.":"Elapsed chapter time as a share of time since the chart start; no birthday or lifetime percentage is inferred.",warnings:[...Q.startKind==="birth"?["The horizon is a display choice, not an estimate of lifespan."]:[],...Q.chapters.some((Z)=>Z.start>V.asOf)?["Some chapters begin after the as-of date and are planned, not elapsed."]:[],...Q.chapters.some((Z)=>Z.parentId!==void 0)?["Parent and child chapters can overlap; chapter percentages are not additive."]:[]]}}var c=`Lifecharts — create, edit, and share a life chart locally
|
|
3
|
+
|
|
4
|
+
Usage: lifecharts <command> [input] [--json] [--today YYYY-MM-DD]
|
|
5
|
+
|
|
6
|
+
create <file|-> Create from friendly JSON or a complete chart document
|
|
7
|
+
compile <file|-> Compile a complete, lossless chart JSON document
|
|
8
|
+
edit <file|-> Alias of compile; preserves all supplied IDs and fields
|
|
9
|
+
inspect <URL|file|-> Print lossless chart JSON; --json includes links and geometry
|
|
10
|
+
validate <file|-> Check friendly creation JSON or complete chart JSON
|
|
11
|
+
verify <URL|file|-> Decode, validate, and check the canonical URL round trip
|
|
12
|
+
templates [list|life|career]
|
|
13
|
+
List starters or print editable illustrative JSON
|
|
14
|
+
--help Show this help
|
|
15
|
+
--version Show the portable format and CLI version
|
|
16
|
+
|
|
17
|
+
Create/compile print a verified share URL; --json also returns edit and embed URLs.
|
|
18
|
+
Input '-' reads stdin. A file can contain one URL, fragment, or JSON document.
|
|
19
|
+
The --today date fixes inspection geometry; it does not change the document.
|
|
20
|
+
No command sends a request or fetches a profile. Quote complete URLs in a shell.
|
|
21
|
+
Keep the returned URL intact. Anyone with it can read the chart; it is not encrypted.
|
|
22
|
+
|
|
23
|
+
Create input: {"name":"Sam","birthDate":"1990-04-12","view":"bars","chapters":[]}
|
|
24
|
+
Birthday is optional. Supply start or a dated chapter when birthDate is unknown.
|
|
25
|
+
For an edit: inspect the URL > chart.json; edit JSON; compile chart.json; verify URL.
|
|
26
|
+
`,BJ=new Set(["create","compile","edit","inspect","validate","verify","templates"]),N=(Q,J=1)=>({ok:!1,stderr:`${Q}
|
|
27
|
+
`,exitCode:J}),F=(Q)=>({ok:!0,stdout:`${typeof Q==="string"?Q:JSON.stringify(Q,null,2)}
|
|
28
|
+
`,exitCode:0});async function YJ(Q,J){let V=Q==="-"?await J.readStdin():Q.startsWith("https://")||Q.startsWith("#")?Q:await J.readFile(Q);if(new TextEncoder().encode(V).byteLength>65536)throw TypeError("Input must be under 64 KB.");return V}async function qJ(Q,J){try{if(!Array.isArray(Q)||Q.some((q)=>typeof q!=="string"))return N("Arguments must be text.",2);if(Q.length===0||Q.length===1&&["--help","help","-h"].includes(Q[0]??""))return F(c.trimEnd());if(Q.length===1&&Q[0]==="--version")return F("Lifecharts CLI 1.0.0 · timeline format 1");let V=Q[0]??"";if(!BJ.has(V))return N("Unknown command. Run --help for available commands.",2);let Z=[],$=!1,Y,K=!1;for(let q=1;q<Q.length;q++){let D=Q[q]??"";if(D==="--json"&&!$)$=!0;else if(D==="--help"&&!K)K=!0;else if(D==="--today"&&Y===void 0){if(Y=Q[++q],!Y||!/^\d{4}-\d{2}-\d{2}$/u.test(Y))return N("--today needs a YYYY-MM-DD date.",2)}else if(D.startsWith("-")&&D!=="-")return N("Unknown or repeated option. Run --help for available options.",2);else Z.push(D)}if(K)return Z.length===0&&!$&&Y===void 0?F(c.trimEnd()):N("Use <command> --help by itself.",2);if(V==="templates"){if(Z.length>1||Y!==void 0)return N("Use templates [list|life|career].",2);let q=Z[0]??"list";if(q==="list")return F($?[{name:"life",description:"Illustrative life with overlapping chapters"},{name:"career",description:"Illustrative career without an inferred birthday"}]:`life Illustrative life with overlapping chapters
|
|
29
|
+
career Illustrative career without an inferred birthday`);if(q==="life")return F(h);if(q==="career")return F({version:1,title:"An example career",start:"2016-06",startKind:"timeline",asOf:"2026-09-01",horizonAge:40,scale:"lived",theme:"system",view:"bars",chapters:[{id:"first-role",label:"First role",start:"2016-06",end:"2020-03",color:"#3478a1"},{id:"next-role",label:"A new direction",start:"2020-03",end:"present",color:"#bd684d"},{id:"side-project",label:"Side project",start:"2023-01",end:"present",color:"#52866b"}]});return N("Unknown template. Run templates list.",2)}if(Z.length!==1)return N("Supply one input file, URL, or '-' for stdin. Run --help for command details.",2);if(Y!==void 0&&!["inspect","verify"].includes(V))return N("--today is supported by inspect and verify only.",2);let j=await YJ(Z[0]??"",J),G;if(["create","compile","edit","validate"].includes(V)){let q;try{q=JSON.parse(j)}catch{return N("The input must be one complete JSON object.")}G=V==="compile"||V==="edit"?U(q):l(q)}else G=d(j);if(!G.ok)return N(G.message);let B=["create","compile","edit"].includes(V)?{...G.value,view:G.value.view??"timeline"}:G.value;if(V==="inspect"&&!$&&Y===void 0)return F(B);if(V==="validate")return F($?{valid:!0,document:B}:`Valid chart: ${B.title} (${B.chapters.length} chapters).`);if(V==="inspect"||V==="verify"){let q=u(B,Y??J.today());return F($?{verified:!0,...q}:V==="inspect"?q:`Verified chart: ${B.title}
|
|
30
|
+
${q.url}`)}let X=C(B);return F($?{verified:!0,document:B,...X}:X.url)}catch(V){return N(V instanceof TypeError?V.message:"The input could not be read. Check the file path, permissions, and JSON.")}}async function o(Q){let J=[],V=0;for await(let Y of Q){if(V+=Y.byteLength,V>65536)throw TypeError("Input must be under 64 KB.");J.push(Y)}let Z=new Uint8Array(V),$=0;for(let Y of J)Z.set(Y,$),$+=Y.byteLength;try{return new TextDecoder("utf-8",{fatal:!0}).decode(Z)}catch{throw TypeError("Input must be valid UTF-8 text.")}}function WJ(){try{return process.argv[1]!==void 0&&p(process.argv[1])===p($J(import.meta.url))}catch{return!1}}if(WJ()){let Q=await qJ(process.argv.slice(2),{readFile:(J)=>o(ZJ(J,{highWaterMark:65537})),readStdin:()=>o(process.stdin),today:()=>new Date().toISOString().slice(0,10)});if(Q.ok)process.stdout.write(Q.stdout);else process.stderr.write(Q.stderr);process.exitCode=Q.exitCode}export{qJ as runLifechartsCli,c as LIFECHARTS_HELP};
|