ccem 1.8.0 → 2.0.0-beta
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/dist/index.js +2849 -24
- package/package.json +22 -15
- package/scripts/convert-ansi-to-ts.js +55 -0
- package/scripts/generate-logo.sh +63 -0
- package/scripts/migrate.js +44 -0
- package/README.md +0 -536
package/dist/index.js
CHANGED
|
@@ -1,25 +1,2850 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as uo}from"commander";import go from"conf";import k from"inquirer";import u from"chalk";import Ot from"cli-table3";import{spawn as It}from"child_process";import*as wt from"fs";import*as ye from"path";import{fileURLToPath as fo}from"url";import le from"crypto";import ee from"fs";import B from"path";var be="aes-256-cbc",De=le.scryptSync("claude-code-env-manager-secret","salt",32),J=e=>{if(!e)return e;let t=le.randomBytes(16),o=le.createCipheriv(be,De,t),s=o.update(e,"utf8","hex");return s+=o.final("hex"),`enc:${t.toString("hex")}:${s}`},Y=e=>{if(!e||!e.startsWith("enc:"))return e;try{let t=e.split(":");if(t.length!==3)return e;let o=Buffer.from(t[1],"hex"),s=t[2],n=le.createDecipheriv(be,De,o),i=n.update(s,"hex","utf8");return i+=n.final("utf8"),i}catch{return e}},Be=()=>{let e=process.cwd(),t=B.parse(e).root;for(;e!==t;){if(ee.existsSync(B.join(e,".git"))||ee.existsSync(B.join(e,"package.json")))return e;e=B.dirname(e)}return process.cwd()},de=(e=!0)=>{let t=Be(),o=B.join(t,".claude"),s=e?"settings.local.json":"settings.json";return B.join(o,s)},$e=()=>{let e=Be(),t=B.join(e,".claude");return ee.existsSync(t)||ee.mkdirSync(t,{recursive:!0}),t},Ae=()=>process.env.HOME||process.env.USERPROFILE||"",Pe=()=>B.join(Ae(),".claude.json"),Se=()=>B.join(Ae(),".claude","settings.json"),Ue=()=>{let e=B.join(Ae(),".claude");return ee.existsSync(e)||ee.mkdirSync(e,{recursive:!0}),e};var Re={GLM:{ANTHROPIC_BASE_URL:"https://open.bigmodel.cn/api/anthropic",ANTHROPIC_MODEL:"glm-4.6",ANTHROPIC_SMALL_FAST_MODEL:"glm-4.5-air"},KIMI:{ANTHROPIC_BASE_URL:"https://api.moonshot.cn/anthropic",ANTHROPIC_MODEL:"kimi-k2-thinking-turbo",ANTHROPIC_SMALL_FAST_MODEL:"kimi-k2-turbo-preview"},MiniMax:{ANTHROPIC_BASE_URL:"https://api.minimaxi.com/anthropic",ANTHROPIC_MODEL:"MiniMax-M2",ANTHROPIC_SMALL_FAST_MODEL:"MiniMax-M2"},DeepSeek:{ANTHROPIC_BASE_URL:"https://api.deepseek.com/anthropic",ANTHROPIC_MODEL:"deepseek-chat",ANTHROPIC_SMALL_FAST_MODEL:"deepseek-chat"}},w={yolo:{name:"YOLO \u6A21\u5F0F",description:"\u5168\u90E8\u653E\u5F00\uFF0C\u65E0\u4EFB\u4F55\u9650\u5236",permissionMode:"bypassPermissions",permissions:{allow:["Bash(*)","Read(*)","Edit(*)","Write(*)","WebFetch(*)","WebSearch(*)","Glob(*)","Grep(*)","LSP(*)","NotebookEdit(*)"],deny:[]}},dev:{name:"\u5F00\u53D1\u6A21\u5F0F",description:"\u65E5\u5E38\u5F00\u53D1\u6743\u9650\uFF0C\u4FDD\u62A4\u654F\u611F\u6587\u4EF6",permissionMode:"acceptEdits",permissions:{allow:["Read(*)","Edit(*)","Write(*)","Glob(*)","Grep(*)","LSP(*)","NotebookEdit(*)","Bash(npm:*)","Bash(pnpm:*)","Bash(yarn:*)","Bash(bun:*)","Bash(node:*)","Bash(npx:*)","Bash(git:*)","Bash(tsc:*)","Bash(tsx:*)","Bash(eslint:*)","Bash(prettier:*)","Bash(jest:*)","Bash(vitest:*)","Bash(cargo:*)","Bash(python:*)","Bash(pip:*)","Bash(go:*)","Bash(make:*)","Bash(cmake:*)","Bash(ls:*)","Bash(cat:*)","Bash(head:*)","Bash(tail:*)","Bash(find:*)","Bash(wc:*)","Bash(mkdir:*)","Bash(cp:*)","Bash(mv:*)","Bash(touch:*)","WebSearch"],deny:["Read(.env)","Read(.env.*)","Read(**/secrets/**)","Read(**/*.pem)","Read(**/*.key)","Read(**/*credential*)","Bash(rm -rf:*)","Bash(sudo:*)","Bash(chmod:*)","Bash(chown:*)"]}},readonly:{name:"\u53EA\u8BFB\u6A21\u5F0F",description:"\u4EC5\u5141\u8BB8\u8BFB\u53D6\u548C\u641C\u7D22\uFF0C\u7981\u6B62\u4EFB\u4F55\u4FEE\u6539",permissionMode:"plan",permissions:{allow:["Read(*)","Glob(*)","Grep(*)","LSP(*)","Bash(git status:*)","Bash(git log:*)","Bash(git diff:*)","Bash(git branch:*)","Bash(git show:*)","Bash(ls:*)","Bash(cat:*)","Bash(head:*)","Bash(tail:*)","Bash(find:*)","Bash(wc:*)","Bash(file:*)","WebSearch"],deny:["Edit(*)","Write(*)","NotebookEdit(*)","Bash(rm:*)","Bash(mv:*)","Bash(cp:*)","Bash(mkdir:*)","Bash(touch:*)","Bash(git add:*)","Bash(git commit:*)","Bash(git push:*)","Bash(git checkout:*)","Bash(git reset:*)","Bash(npm install:*)","Bash(pnpm install:*)","Bash(yarn add:*)"]}},safe:{name:"\u5B89\u5168\u6A21\u5F0F",description:"\u4FDD\u5B88\u6743\u9650\uFF0C\u9002\u5408\u4E0D\u719F\u6089\u7684\u4EE3\u7801\u5E93",permissionMode:"default",permissions:{allow:["Read(*)","Glob(*)","Grep(*)","LSP(*)","Bash(git status:*)","Bash(git log:*)","Bash(git diff:*)","Bash(ls:*)","Bash(cat:*)","Bash(head:*)","Bash(tail:*)","Bash(find:*)","Bash(wc:*)"],deny:["Read(.env)","Read(.env.*)","Read(**/secrets/**)","Read(**/*.pem)","Read(**/*.key)","Read(**/*credential*)","Read(**/*password*)","Edit(*)","Write(*)","NotebookEdit(*)","Bash(curl:*)","Bash(wget:*)","Bash(ssh:*)","Bash(scp:*)","Bash(rm:*)","Bash(mv:*)","WebFetch(*)"]}},ci:{name:"CI/CD \u6A21\u5F0F",description:"\u9002\u5408\u81EA\u52A8\u5316\u6D41\u6C34\u7EBF\u7684\u6743\u9650",permissionMode:"default",permissions:{allow:["Read(*)","Edit(*)","Write(*)","Glob(*)","Grep(*)","LSP(*)","Bash(npm:*)","Bash(pnpm:*)","Bash(yarn:*)","Bash(node:*)","Bash(git:*)","Bash(docker:*)","Bash(make:*)","Bash(cargo:*)","Bash(go:*)","Bash(python:*)","Bash(pip:*)","Bash(pytest:*)","Bash(jest:*)","Bash(vitest:*)"],deny:["Read(.env.local)","Read(**/secrets/**)","Bash(sudo:*)","Bash(ssh:*)","Bash(scp:*)","WebFetch(*)","WebSearch"]}},audit:{name:"\u5BA1\u8BA1\u6A21\u5F0F",description:"\u4EC5\u8BFB\u53D6\u548C\u641C\u7D22\uFF0C\u7528\u4E8E\u5B89\u5168\u5BA1\u8BA1",permissionMode:"plan",permissions:{allow:["Read(*)","Glob(*)","Grep(*)","LSP(*)","Bash(git log:*)","Bash(git blame:*)","Bash(git show:*)","Bash(git diff:*)","Bash(ls:*)","Bash(find:*)","Bash(wc:*)","Bash(file:*)","Bash(stat:*)"],deny:["Edit(*)","Write(*)","NotebookEdit(*)","Bash(rm:*)","Bash(mv:*)","Bash(cp:*)","Bash(curl:*)","Bash(wget:*)","Bash(ssh:*)","WebFetch(*)"]}}},Ee=()=>Object.keys(w);import D from"chalk";import Ge from"cli-table3";import*as z from"fs";import*as M from"fs/promises";import*as G from"path";import*as Ce from"os";import*as Fe from"readline";var Te=G.join(Ce.homedir(),".claude","projects"),pe=G.join(Ce.homedir(),".ccem"),Oe=1,Ie=()=>G.join(pe,"usage-cache.json"),vt=()=>G.join(pe,"model-prices.json");async function je(){try{await M.access(pe)}catch{await M.mkdir(pe,{recursive:!0})}}var Ht="https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",bt=()=>G.join(__dirname,"..","model-prices.json"),oe={"claude-opus-4-5":{input_cost_per_token:5e-6,output_cost_per_token:25e-6,cache_read_input_token_cost:5e-7,cache_creation_input_token_cost:625e-8},"claude-sonnet-4-5":{input_cost_per_token:3e-6,output_cost_per_token:15e-6,cache_read_input_token_cost:3e-7,cache_creation_input_token_cost:375e-8},"claude-haiku-4-5":{input_cost_per_token:1e-6,output_cost_per_token:5e-6,cache_read_input_token_cost:1e-7,cache_creation_input_token_cost:125e-8}};function me(e){return e.replace(/-20\d{6}.*$/,"").replace(/-v\d+:\d+$/,"").replace(/^anthropic\./,"").replace(/^vertex_ai\//,"").replace(/@.*$/,"")}var te=null;async function Dt(){if(te)return te;await je();let e=vt();try{let t=await fetch(Ht,{signal:AbortSignal.timeout(1e3)});if(t.ok){let o=await t.json(),s={};for(let[n,i]of Object.entries(o))i.input_cost_per_token&&i.output_cost_per_token&&(s[n]={input_cost_per_token:i.input_cost_per_token,output_cost_per_token:i.output_cost_per_token,cache_read_input_token_cost:i.cache_read_input_token_cost,cache_creation_input_token_cost:i.cache_creation_input_token_cost});return await M.writeFile(e,JSON.stringify(s,null,2)),te=s,s}}catch{}try{let t=await M.readFile(e,"utf-8"),o=JSON.parse(t);return te=o,o}catch{}try{let t=bt(),o=await M.readFile(t,"utf-8"),s=JSON.parse(o);return te=s,s}catch{}return te=oe,oe}function Bt(e,t){if(t[e])return t[e];let o=me(e);if(t[o])return t[o];for(let[s,n]of Object.entries(t))if(s.includes(o)||o.includes(me(s)))return n;return e.includes("opus")?oe["claude-opus-4-5"]:e.includes("sonnet")?oe["claude-sonnet-4-5"]:e.includes("haiku")?oe["claude-haiku-4-5"]:oe["claude-sonnet-4-5"]}function $t(e,t){return e.inputTokens*t.input_cost_per_token+e.outputTokens*t.output_cost_per_token+e.cacheReadTokens*(t.cache_read_input_token_cost||0)+e.cacheCreationTokens*(t.cache_creation_input_token_cost||0)}function $(){return{inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0,cost:0}}function U(e,t){return{inputTokens:e.inputTokens+t.inputTokens,outputTokens:e.outputTokens+t.outputTokens,cacheReadTokens:e.cacheReadTokens+t.cacheReadTokens,cacheCreationTokens:e.cacheCreationTokens+t.cacheCreationTokens,cost:e.cost+t.cost}}async function Ut(e){try{let t=await M.stat(e);return{mtime:t.mtimeMs,size:t.size}}catch{return null}}function Ft(){try{let e=Ie();if(!z.existsSync(e))return null;let t=JSON.parse(z.readFileSync(e,"utf-8"));return t.version!==Oe?null:t}catch{return null}}async function jt(){try{let e=Ie(),t=await M.readFile(e,"utf-8"),o=JSON.parse(t);return o.version!==Oe?null:o}catch{return null}}function Ke(){let e=Ft();if(!e)return null;let t=new Date,o=new Date(t.getFullYear(),t.getMonth(),t.getDate()),s=new Date(o);s.setDate(s.getDate()-s.getDay());let n={today:$(),week:$(),total:$(),dailyHistory:{},byModel:{},lastUpdated:t.toISOString()};for(let i of Object.values(e.files))for(let a of i.stats.entries){let l=new Date(a.timestamp),c=a.timestamp.split("T")[0];n.total=U(n.total,a.usage),n.dailyHistory[c]||(n.dailyHistory[c]=$()),n.dailyHistory[c]=U(n.dailyHistory[c],a.usage);let p=me(a.model);n.byModel[p]||(n.byModel[p]=$()),n.byModel[p]=U(n.byModel[p],a.usage),l>=o&&(n.today=U(n.today,a.usage)),l>=s&&(n.week=U(n.week,a.usage))}return n}async function Kt(e){try{await je(),await M.writeFile(Ie(),JSON.stringify(e,null,2))}catch{}}async function Yt(e,t,o){let s=[];try{let n=z.createReadStream(e,{encoding:"utf-8"}),i=Fe.createInterface({input:n,crlfDelay:1/0}),a=0;for await(let l of i){if(a++,a%100===0){if(o?.aborted)throw i.close(),n.destroy(),new Error("Aborted");a%1e3===0&&await new Promise(c=>setTimeout(c,0))}if(l.trim())try{let c=JSON.parse(l);if(c.type!=="assistant"||!c.message?.usage)continue;let p=c.message.model||"unknown",d=c.message.usage,_={inputTokens:d.input_tokens||0,outputTokens:d.output_tokens||0,cacheReadTokens:d.cache_read_input_tokens||0,cacheCreationTokens:d.cache_creation_input_tokens||0},m=Bt(p,t),g=$t(_,m);s.push({timestamp:c.timestamp||new Date().toISOString(),model:p,usage:{..._,cost:g}})}catch{}}}catch(n){if(n.message==="Aborted")throw n}return{entries:s}}async function Gt(){let e=[];try{if(!await M.access(Te).then(()=>!0).catch(()=>!1))return e;let o=await M.readdir(Te);for(let s of o){let n=G.join(Te,s);try{if((await M.stat(n)).isDirectory()){let a=await M.readdir(n);for(let l of a)l.endsWith(".jsonl")&&e.push(G.join(n,l))}}catch{}}}catch{}return e}async function Ye(e){if(e?.aborted)throw new Error("Aborted");let t=await Dt();if(e?.aborted)throw new Error("Aborted");let o=await Gt();if(e?.aborted)throw new Error("Aborted");let s=await jt(),n={version:Oe,files:{},lastUpdated:new Date().toISOString()},i=[],a=5,l=[];for(let m=0;m<o.length;m+=a)l.push(o.slice(m,m+a));for(let m of l){if(e?.aborted)throw new Error("Aborted");let g=m.map(async y=>{let A=await Ut(y);if(!A)return null;let C=s?.files[y],T=C&&C.meta.mtime===A.mtime&&C.meta.size===A.size,O;return T?O=C.stats:(O=await Yt(y,t,e),await new Promise(R=>setTimeout(R,0))),{file:y,meta:A,stats:O}}),h=await Promise.all(g);for(let y of h)y&&(n.files[y.file]={meta:y.meta,stats:y.stats},i.push(...y.stats.entries))}e?.aborted||Kt(n).catch(()=>{});let c=new Date,p=new Date(c.getFullYear(),c.getMonth(),c.getDate()),d=new Date(p);d.setDate(d.getDate()-d.getDay());let _={today:$(),week:$(),total:$(),dailyHistory:{},byModel:{},lastUpdated:c.toISOString()};for(let m of i){let g=new Date(m.timestamp),h=m.timestamp.split("T")[0];_.total=U(_.total,m.usage),_.dailyHistory[h]||(_.dailyHistory[h]=$()),_.dailyHistory[h]=U(_.dailyHistory[h],m.usage);let y=me(m.model);_.byModel[y]||(_.byModel[y]=$()),_.byModel[y]=U(_.byModel[y],m.usage),g>=p&&(_.today=U(_.today,m.usage)),g>=d&&(_.week=U(_.week,m.usage))}return _}function b(e){return e>=1e6?(e/1e6).toFixed(1)+"M":e>=1e3?(e/1e3).toFixed(1)+"K":e.toString()}function ne(e){return e>=1||e>=.01?"$"+e.toFixed(2):"$"+e.toFixed(4)}function q(e){return e.inputTokens+e.outputTokens+e.cacheReadTokens+e.cacheCreationTokens}var r={primary:D.hex("#89B4FA"),accent:D.hex("#CBA6F7"),success:D.hex("#A6E3A1"),warning:D.hex("#F9E2AF"),danger:D.hex("#F38BA8"),text:D.white,muted:D.hex("#6C7086"),dim:D.hex("#45475A")},We=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],we=0,re=null,Wt=()=>r.primary(We[we]),Je=e=>{re||(re=setInterval(()=>{we=(we+1)%We.length,e()},80))},ue=()=>{re&&(clearInterval(re),re=null)};var ze=()=>process.stdout.columns||80,qe=(e,t,o)=>{let s=D.hex("#89B4FA"),n=D.hex("#45475A"),i=D.hex("#6C7086"),a=ze(),l=a<60,c=a<45,p=r.primary("CCEM")+" "+r.muted("Claude Code Env Manager"),d=r.primary("CCEM"),_=r.muted("Env: ")+r.primary(e),m=t.ANTHROPIC_BASE_URL||"-",g=t.ANTHROPIC_MODEL||"-",h=t.ANTHROPIC_SMALL_FAST_MODEL||"-",y=t.ANTHROPIC_API_KEY?t.ANTHROPIC_API_KEY.slice(0,2)+"\u2022\u2022\u2022\u2022"+t.ANTHROPIC_API_KEY.slice(-4):"-",A=(I,K)=>I.length>K?I.slice(0,K-3)+"...":I,C=(I,K)=>{if(I.length<=K)return I;try{let j=new URL(I),Mt=j.protocol+"//",He=j.host,_e=j.pathname+j.search,kt=He.slice(0,8),Nt=He.slice(-4),xt=_e.length>10?_e.slice(0,7)+"...":_e;return`${Mt}${kt}...${Nt}${xt}`}catch{return A(I,K)}},T=7,O;l?O=[_,r.muted("Model:".padEnd(T))+r.dim(A(g,25)),r.muted("Key:".padEnd(T))+r.dim(y)]:O=[_+(o&&w[o]?" "+r.accent(`[${w[o].name}]`):""),r.muted("URL:".padEnd(T))+r.dim(C(m,40)),r.muted("Model:".padEnd(T))+r.dim(A(g,15))+" "+r.muted("Fast:".padEnd(T))+r.dim(A(h,15)),r.muted("Key:".padEnd(T))+r.dim(y)];let R=[];if(c)R.push(` ${i("*")} ${n("\u2584\u2588\u2584")} ${i("*")}`),R.push(` ${i("*")} ${s("\u2590\u259B\u2588\u2588\u2588\u2588\u2588\u259C\u258C")} ${i("*")}`),R.push(` ${i("*")} ${s("\u259D\u259C\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u259B\u2598")} ${i("*")}`),R.push(` ${i("*")} ${s("\u2598\u2598 \u259D\u259D")} ${i("*")}`),R.push(""),R.push(d),R.push(""),O.forEach(I=>R.push(I));else{let I=l?" ":" ";R.push(""),R.push(` ${n("\u2584\u2588\u2588\u2584")} ${I}${l?d:p}`),R.push(` ${i("*")} ${s("\u2590\u259B\u2588\u2588\u2588\u2588\u2588\u259C\u258C")} ${i("*")} ${I}${O[0]||""}`),R.push(` ${i("*")} ${s("\u259D\u259C\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u259B\u2598")} ${i("*")} ${I}${O[1]||""}`),R.push(` ${i("*")} ${s("\u2598\u2598 \u259D\u259D")} ${i("*")} ${I}${O[2]||""}`),O[3]&&R.push(` ${I}${O[3]}`)}return R.join(`
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import Conf2 from "conf";
|
|
6
|
+
import inquirer from "inquirer";
|
|
7
|
+
import chalk7 from "chalk";
|
|
8
|
+
import Table3 from "cli-table3";
|
|
9
|
+
import { spawn as spawn3 } from "child_process";
|
|
10
|
+
import * as fs7 from "fs";
|
|
11
|
+
import * as path5 from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { encrypt as encrypt2, decrypt as decrypt2, ENV_PRESETS, PERMISSION_PRESETS as PERMISSION_PRESETS4, getCcemConfigDir, ensureCcemDir as ensureCcemDir3, getCcemConfigPath, getLegacyConfigPath } from "@ccem/core";
|
|
14
|
+
|
|
15
|
+
// src/ui.ts
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
import Table from "cli-table3";
|
|
18
|
+
import { PERMISSION_PRESETS } from "@ccem/core";
|
|
19
|
+
|
|
20
|
+
// src/usage.ts
|
|
21
|
+
import * as fs from "fs";
|
|
22
|
+
import * as fsPromises from "fs/promises";
|
|
23
|
+
import * as path from "path";
|
|
24
|
+
import * as os from "os";
|
|
25
|
+
import * as readline from "readline";
|
|
26
|
+
var CLAUDE_PROJECTS_DIR = path.join(os.homedir(), ".claude", "projects");
|
|
27
|
+
var CCEM_DIR = path.join(os.homedir(), ".ccem");
|
|
28
|
+
var CACHE_VERSION = 1;
|
|
29
|
+
var getCachePath = () => path.join(CCEM_DIR, "usage-cache.json");
|
|
30
|
+
var getPricesPath = () => path.join(CCEM_DIR, "model-prices.json");
|
|
31
|
+
async function ensureCcemDir() {
|
|
32
|
+
try {
|
|
33
|
+
await fsPromises.access(CCEM_DIR);
|
|
34
|
+
} catch {
|
|
35
|
+
await fsPromises.mkdir(CCEM_DIR, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
var LITELLM_PRICES_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
|
|
39
|
+
var getBundledPricesPath = () => {
|
|
40
|
+
return path.join(__dirname, "..", "model-prices.json");
|
|
41
|
+
};
|
|
42
|
+
var DEFAULT_PRICES = {
|
|
43
|
+
"claude-opus-4-5": {
|
|
44
|
+
input_cost_per_token: 5e-6,
|
|
45
|
+
output_cost_per_token: 25e-6,
|
|
46
|
+
cache_read_input_token_cost: 5e-7,
|
|
47
|
+
cache_creation_input_token_cost: 625e-8
|
|
48
|
+
},
|
|
49
|
+
"claude-sonnet-4-5": {
|
|
50
|
+
input_cost_per_token: 3e-6,
|
|
51
|
+
output_cost_per_token: 15e-6,
|
|
52
|
+
cache_read_input_token_cost: 3e-7,
|
|
53
|
+
cache_creation_input_token_cost: 375e-8
|
|
54
|
+
},
|
|
55
|
+
"claude-haiku-4-5": {
|
|
56
|
+
input_cost_per_token: 1e-6,
|
|
57
|
+
output_cost_per_token: 5e-6,
|
|
58
|
+
cache_read_input_token_cost: 1e-7,
|
|
59
|
+
cache_creation_input_token_cost: 125e-8
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
function normalizeModelName(model) {
|
|
63
|
+
const normalized = model.replace(/-20\d{6}.*$/, "").replace(/-v\d+:\d+$/, "").replace(/^anthropic\./, "").replace(/^vertex_ai\//, "").replace(/@.*$/, "");
|
|
64
|
+
return normalized;
|
|
65
|
+
}
|
|
66
|
+
var pricesCache = null;
|
|
67
|
+
async function loadPrices() {
|
|
68
|
+
if (pricesCache) return pricesCache;
|
|
69
|
+
await ensureCcemDir();
|
|
70
|
+
const pricesPath = getPricesPath();
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetch(LITELLM_PRICES_URL, { signal: AbortSignal.timeout(1e3) });
|
|
73
|
+
if (response.ok) {
|
|
74
|
+
const data = await response.json();
|
|
75
|
+
const prices = {};
|
|
76
|
+
for (const [key, value] of Object.entries(data)) {
|
|
77
|
+
if (value.input_cost_per_token && value.output_cost_per_token) {
|
|
78
|
+
prices[key] = {
|
|
79
|
+
input_cost_per_token: value.input_cost_per_token,
|
|
80
|
+
output_cost_per_token: value.output_cost_per_token,
|
|
81
|
+
cache_read_input_token_cost: value.cache_read_input_token_cost,
|
|
82
|
+
cache_creation_input_token_cost: value.cache_creation_input_token_cost
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
await fsPromises.writeFile(pricesPath, JSON.stringify(prices, null, 2));
|
|
87
|
+
pricesCache = prices;
|
|
88
|
+
return prices;
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const content = await fsPromises.readFile(pricesPath, "utf-8");
|
|
94
|
+
const data = JSON.parse(content);
|
|
95
|
+
pricesCache = data;
|
|
96
|
+
return data;
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const bundledPath = getBundledPricesPath();
|
|
101
|
+
const content = await fsPromises.readFile(bundledPath, "utf-8");
|
|
102
|
+
const data = JSON.parse(content);
|
|
103
|
+
pricesCache = data;
|
|
104
|
+
return data;
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
pricesCache = DEFAULT_PRICES;
|
|
108
|
+
return DEFAULT_PRICES;
|
|
109
|
+
}
|
|
110
|
+
function getModelPrice(model, prices) {
|
|
111
|
+
if (prices[model]) return prices[model];
|
|
112
|
+
const normalized = normalizeModelName(model);
|
|
113
|
+
if (prices[normalized]) return prices[normalized];
|
|
114
|
+
for (const [key, value] of Object.entries(prices)) {
|
|
115
|
+
if (key.includes(normalized) || normalized.includes(normalizeModelName(key))) {
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (model.includes("opus")) return DEFAULT_PRICES["claude-opus-4-5"];
|
|
120
|
+
if (model.includes("sonnet")) return DEFAULT_PRICES["claude-sonnet-4-5"];
|
|
121
|
+
if (model.includes("haiku")) return DEFAULT_PRICES["claude-haiku-4-5"];
|
|
122
|
+
return DEFAULT_PRICES["claude-sonnet-4-5"];
|
|
123
|
+
}
|
|
124
|
+
function calculateCost(usage, price) {
|
|
125
|
+
return usage.inputTokens * price.input_cost_per_token + usage.outputTokens * price.output_cost_per_token + usage.cacheReadTokens * (price.cache_read_input_token_cost || 0) + usage.cacheCreationTokens * (price.cache_creation_input_token_cost || 0);
|
|
126
|
+
}
|
|
127
|
+
function emptyUsage() {
|
|
128
|
+
return {
|
|
129
|
+
inputTokens: 0,
|
|
130
|
+
outputTokens: 0,
|
|
131
|
+
cacheReadTokens: 0,
|
|
132
|
+
cacheCreationTokens: 0,
|
|
133
|
+
cost: 0
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function mergeUsage(a, b) {
|
|
137
|
+
return {
|
|
138
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
139
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
140
|
+
cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
|
|
141
|
+
cacheCreationTokens: a.cacheCreationTokens + b.cacheCreationTokens,
|
|
142
|
+
cost: a.cost + b.cost
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async function getFileMetaAsync(filePath) {
|
|
146
|
+
try {
|
|
147
|
+
const stat2 = await fsPromises.stat(filePath);
|
|
148
|
+
return {
|
|
149
|
+
mtime: stat2.mtimeMs,
|
|
150
|
+
size: stat2.size
|
|
151
|
+
};
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function loadCacheSync() {
|
|
157
|
+
try {
|
|
158
|
+
const cachePath = getCachePath();
|
|
159
|
+
if (!fs.existsSync(cachePath)) return null;
|
|
160
|
+
const data = JSON.parse(fs.readFileSync(cachePath, "utf-8"));
|
|
161
|
+
if (data.version !== CACHE_VERSION) return null;
|
|
162
|
+
return data;
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function loadCacheAsync() {
|
|
168
|
+
try {
|
|
169
|
+
const cachePath = getCachePath();
|
|
170
|
+
const content = await fsPromises.readFile(cachePath, "utf-8");
|
|
171
|
+
const data = JSON.parse(content);
|
|
172
|
+
if (data.version !== CACHE_VERSION) return null;
|
|
173
|
+
return data;
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function getUsageStatsFromCache() {
|
|
179
|
+
const cache = loadCacheSync();
|
|
180
|
+
if (!cache) return null;
|
|
181
|
+
const now = /* @__PURE__ */ new Date();
|
|
182
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
183
|
+
const weekStart = new Date(todayStart);
|
|
184
|
+
weekStart.setDate(weekStart.getDate() - weekStart.getDay());
|
|
185
|
+
const stats = {
|
|
186
|
+
today: emptyUsage(),
|
|
187
|
+
week: emptyUsage(),
|
|
188
|
+
total: emptyUsage(),
|
|
189
|
+
dailyHistory: {},
|
|
190
|
+
byModel: {},
|
|
191
|
+
lastUpdated: now.toISOString()
|
|
192
|
+
};
|
|
193
|
+
for (const fileData of Object.values(cache.files)) {
|
|
194
|
+
for (const entry of fileData.stats.entries) {
|
|
195
|
+
const entryTime = new Date(entry.timestamp);
|
|
196
|
+
const dateKey = entry.timestamp.split("T")[0];
|
|
197
|
+
stats.total = mergeUsage(stats.total, entry.usage);
|
|
198
|
+
if (!stats.dailyHistory[dateKey]) {
|
|
199
|
+
stats.dailyHistory[dateKey] = emptyUsage();
|
|
200
|
+
}
|
|
201
|
+
stats.dailyHistory[dateKey] = mergeUsage(stats.dailyHistory[dateKey], entry.usage);
|
|
202
|
+
const normalizedModel = normalizeModelName(entry.model);
|
|
203
|
+
if (!stats.byModel[normalizedModel]) {
|
|
204
|
+
stats.byModel[normalizedModel] = emptyUsage();
|
|
205
|
+
}
|
|
206
|
+
stats.byModel[normalizedModel] = mergeUsage(stats.byModel[normalizedModel], entry.usage);
|
|
207
|
+
if (entryTime >= todayStart) {
|
|
208
|
+
stats.today = mergeUsage(stats.today, entry.usage);
|
|
209
|
+
}
|
|
210
|
+
if (entryTime >= weekStart) {
|
|
211
|
+
stats.week = mergeUsage(stats.week, entry.usage);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return stats;
|
|
216
|
+
}
|
|
217
|
+
async function saveCacheAsync(cache) {
|
|
218
|
+
try {
|
|
219
|
+
await ensureCcemDir();
|
|
220
|
+
await fsPromises.writeFile(getCachePath(), JSON.stringify(cache, null, 2));
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function parseJSONLFileAsync(filePath, prices, signal) {
|
|
225
|
+
const entries = [];
|
|
226
|
+
try {
|
|
227
|
+
const fileStream = fs.createReadStream(filePath, { encoding: "utf-8" });
|
|
228
|
+
const rl = readline.createInterface({
|
|
229
|
+
input: fileStream,
|
|
230
|
+
crlfDelay: Infinity
|
|
231
|
+
});
|
|
232
|
+
let lineCount = 0;
|
|
233
|
+
for await (const line of rl) {
|
|
234
|
+
lineCount++;
|
|
235
|
+
if (lineCount % 100 === 0) {
|
|
236
|
+
if (signal?.aborted) {
|
|
237
|
+
rl.close();
|
|
238
|
+
fileStream.destroy();
|
|
239
|
+
throw new Error("Aborted");
|
|
240
|
+
}
|
|
241
|
+
if (lineCount % 1e3 === 0) {
|
|
242
|
+
await new Promise((resolve2) => setTimeout(resolve2, 0));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!line.trim()) continue;
|
|
246
|
+
try {
|
|
247
|
+
const entry = JSON.parse(line);
|
|
248
|
+
if (entry.type !== "assistant" || !entry.message?.usage) continue;
|
|
249
|
+
const model = entry.message.model || "unknown";
|
|
250
|
+
const rawUsage = entry.message.usage;
|
|
251
|
+
const usage = {
|
|
252
|
+
inputTokens: rawUsage.input_tokens || 0,
|
|
253
|
+
outputTokens: rawUsage.output_tokens || 0,
|
|
254
|
+
cacheReadTokens: rawUsage.cache_read_input_tokens || 0,
|
|
255
|
+
cacheCreationTokens: rawUsage.cache_creation_input_tokens || 0
|
|
256
|
+
};
|
|
257
|
+
const price = getModelPrice(model, prices);
|
|
258
|
+
const cost = calculateCost(usage, price);
|
|
259
|
+
entries.push({
|
|
260
|
+
timestamp: entry.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
261
|
+
model,
|
|
262
|
+
usage: { ...usage, cost }
|
|
263
|
+
});
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch (err) {
|
|
268
|
+
if (err.message === "Aborted") {
|
|
269
|
+
throw err;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return { entries };
|
|
273
|
+
}
|
|
274
|
+
async function getAllJSONLFilesAsync() {
|
|
275
|
+
const files = [];
|
|
276
|
+
try {
|
|
277
|
+
const projectsDirExists = await fsPromises.access(CLAUDE_PROJECTS_DIR).then(() => true).catch(() => false);
|
|
278
|
+
if (!projectsDirExists) return files;
|
|
279
|
+
const projects = await fsPromises.readdir(CLAUDE_PROJECTS_DIR);
|
|
280
|
+
for (const project of projects) {
|
|
281
|
+
const projectPath = path.join(CLAUDE_PROJECTS_DIR, project);
|
|
282
|
+
try {
|
|
283
|
+
const stat2 = await fsPromises.stat(projectPath);
|
|
284
|
+
if (stat2.isDirectory()) {
|
|
285
|
+
const projectFiles = await fsPromises.readdir(projectPath);
|
|
286
|
+
for (const file of projectFiles) {
|
|
287
|
+
if (file.endsWith(".jsonl")) {
|
|
288
|
+
files.push(path.join(projectPath, file));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
return files;
|
|
298
|
+
}
|
|
299
|
+
async function getUsageStats(signal) {
|
|
300
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
301
|
+
const prices = await loadPrices();
|
|
302
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
303
|
+
const files = await getAllJSONLFilesAsync();
|
|
304
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
305
|
+
const cache = await loadCacheAsync();
|
|
306
|
+
const newCache = {
|
|
307
|
+
version: CACHE_VERSION,
|
|
308
|
+
files: {},
|
|
309
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
310
|
+
};
|
|
311
|
+
const allEntries = [];
|
|
312
|
+
const CONCURRENCY_LIMIT = 5;
|
|
313
|
+
const chunks = [];
|
|
314
|
+
for (let i = 0; i < files.length; i += CONCURRENCY_LIMIT) {
|
|
315
|
+
chunks.push(files.slice(i, i + CONCURRENCY_LIMIT));
|
|
316
|
+
}
|
|
317
|
+
for (const chunk of chunks) {
|
|
318
|
+
if (signal?.aborted) {
|
|
319
|
+
throw new Error("Aborted");
|
|
320
|
+
}
|
|
321
|
+
const chunkPromises = chunk.map(async (file) => {
|
|
322
|
+
const meta = await getFileMetaAsync(file);
|
|
323
|
+
if (!meta) return null;
|
|
324
|
+
const cachedFile = cache?.files[file];
|
|
325
|
+
const cacheValid = cachedFile && cachedFile.meta.mtime === meta.mtime && cachedFile.meta.size === meta.size;
|
|
326
|
+
let fileStats;
|
|
327
|
+
if (cacheValid) {
|
|
328
|
+
fileStats = cachedFile.stats;
|
|
329
|
+
} else {
|
|
330
|
+
fileStats = await parseJSONLFileAsync(file, prices, signal);
|
|
331
|
+
await new Promise((resolve2) => setTimeout(resolve2, 0));
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
file,
|
|
335
|
+
meta,
|
|
336
|
+
stats: fileStats
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
const chunkResults = await Promise.all(chunkPromises);
|
|
340
|
+
for (const result of chunkResults) {
|
|
341
|
+
if (!result) continue;
|
|
342
|
+
newCache.files[result.file] = {
|
|
343
|
+
meta: result.meta,
|
|
344
|
+
stats: result.stats
|
|
345
|
+
};
|
|
346
|
+
allEntries.push(...result.stats.entries);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (!signal?.aborted) {
|
|
350
|
+
saveCacheAsync(newCache).catch(() => {
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
const now = /* @__PURE__ */ new Date();
|
|
354
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
355
|
+
const weekStart = new Date(todayStart);
|
|
356
|
+
weekStart.setDate(weekStart.getDate() - weekStart.getDay());
|
|
357
|
+
const stats = {
|
|
358
|
+
today: emptyUsage(),
|
|
359
|
+
week: emptyUsage(),
|
|
360
|
+
total: emptyUsage(),
|
|
361
|
+
dailyHistory: {},
|
|
362
|
+
byModel: {},
|
|
363
|
+
lastUpdated: now.toISOString()
|
|
364
|
+
};
|
|
365
|
+
for (const entry of allEntries) {
|
|
366
|
+
const entryTime = new Date(entry.timestamp);
|
|
367
|
+
const dateKey = entry.timestamp.split("T")[0];
|
|
368
|
+
stats.total = mergeUsage(stats.total, entry.usage);
|
|
369
|
+
if (!stats.dailyHistory[dateKey]) {
|
|
370
|
+
stats.dailyHistory[dateKey] = emptyUsage();
|
|
371
|
+
}
|
|
372
|
+
stats.dailyHistory[dateKey] = mergeUsage(stats.dailyHistory[dateKey], entry.usage);
|
|
373
|
+
const normalizedModel = normalizeModelName(entry.model);
|
|
374
|
+
if (!stats.byModel[normalizedModel]) {
|
|
375
|
+
stats.byModel[normalizedModel] = emptyUsage();
|
|
376
|
+
}
|
|
377
|
+
stats.byModel[normalizedModel] = mergeUsage(stats.byModel[normalizedModel], entry.usage);
|
|
378
|
+
if (entryTime >= todayStart) {
|
|
379
|
+
stats.today = mergeUsage(stats.today, entry.usage);
|
|
380
|
+
}
|
|
381
|
+
if (entryTime >= weekStart) {
|
|
382
|
+
stats.week = mergeUsage(stats.week, entry.usage);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return stats;
|
|
386
|
+
}
|
|
387
|
+
function formatTokens(n) {
|
|
388
|
+
if (n >= 1e6) {
|
|
389
|
+
return (n / 1e6).toFixed(1) + "M";
|
|
390
|
+
}
|
|
391
|
+
if (n >= 1e3) {
|
|
392
|
+
return (n / 1e3).toFixed(1) + "K";
|
|
393
|
+
}
|
|
394
|
+
return n.toString();
|
|
395
|
+
}
|
|
396
|
+
function formatCost(n) {
|
|
397
|
+
if (n >= 1) {
|
|
398
|
+
return "$" + n.toFixed(2);
|
|
399
|
+
}
|
|
400
|
+
if (n >= 0.01) {
|
|
401
|
+
return "$" + n.toFixed(2);
|
|
402
|
+
}
|
|
403
|
+
return "$" + n.toFixed(4);
|
|
404
|
+
}
|
|
405
|
+
function getTotalTokens(usage) {
|
|
406
|
+
return usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheCreationTokens;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// src/logo-generated.ts
|
|
410
|
+
var LOGO_MINIMAL = {
|
|
411
|
+
width: 8,
|
|
412
|
+
height: 5,
|
|
413
|
+
ansi: ` \x1B[38;5;1m \x1B[38;5;103m\u2582\x1B[38;5;109m\u2584\x1B[7m\x1B[38;5;60m\u2583\x1B[38;5;61m\u2598\x1B[0m\x1B[38;5;60;48;5;67m\u2583\x1B[38;5;23m\u2596\x1B[38;5;67;48;5;239m\u2586\x1B[0m\x1B[38;5;60m\u259E \x1B[7m\x1B[38;5;24m\u2586\x1B[38;5;25m\u2585\x1B[38;5;24m\u2583\u2582\x1B[0m\x1B[38;5;1m \x1B[0m
|
|
414
|
+
\x1B[38;5;1m \x1B[38;5;103m\u2584\x1B[7m\u2582\x1B[38;5;110m\u2585\x1B[0m\x1B[38;5;60m\u2597\x1B[38;5;67m\u259E\x1B[7m\x1B[38;5;240m\u2586\x1B[38;5;25m\u258D\x1B[0m\x1B[38;5;23m\u258E\x1B[38;5;67m\u258C\x1B[7m\u2596\x1B[0m\x1B[38;5;67m\u2596\x1B[7m\x1B[38;5;60m\u2584\x1B[0m\x1B[38;5;1m \x1B[0m
|
|
415
|
+
\x1B[38;5;1m \x1B[38;5;60m\u2597\x1B[7m\u2584\x1B[0m\x1B[38;5;1m \x1B[7m\x1B[38;5;25m\u258F\x1B[0m\x1B[38;5;1m \x1B[7m\x1B[38;5;67m\u2583\x1B[0m\x1B[38;5;1m \x1B[0m
|
|
416
|
+
\x1B[38;5;1m \x1B[7m\x1B[38;5;25m\u2585\x1B[0m\x1B[38;5;1m \x1B[0m
|
|
417
|
+
\x1B[?25h`
|
|
418
|
+
};
|
|
419
|
+
var LOGO_COMPACT = {
|
|
420
|
+
width: 12,
|
|
421
|
+
height: 6,
|
|
422
|
+
ansi: ` \x1B[38;5;1m \x1B[38;5;239m\u2581\x1B[38;5;60m\u2583 \x1B[38;5;103m\u2581\x1B[38;5;110m\u2584 \x1B[38;5;61m\u2586\x1B[38;5;67m\u2586\u2586\x1B[38;5;68m\u2586\x1B[38;5;67m\u2582\x1B[38;5;66m\u2584 \x1B[38;5;23m\u259D\x1B[7m\x1B[38;5;24m\u2583\x1B[0m\x1B[38;5;24m\u2586\x1B[38;5;25m\u2585\u2584 \x1B[0m
|
|
423
|
+
\x1B[38;5;1m \x1B[38;5;66m\u2581\x1B[38;5;110m\u2584\u2586\x1B[7m\u2584\x1B[0m\x1B[38;5;239m\u2597\x1B[38;5;68m\u259E\x1B[7m\x1B[38;5;67m\u2585\x1B[38;5;25m\u258D\x1B[0m\x1B[38;5;25m\u258E\x1B[38;5;109m\u258C\x1B[38;5;239;48;5;68m\u2596\x1B[0m\x1B[38;5;67m\u2596\x1B[7m\u2585\x1B[0m\x1B[38;5;66m\u2585 \x1B[0m
|
|
424
|
+
\x1B[38;5;1m \x1B[7m\x1B[38;5;110m\u2583\x1B[38;5;67m\u2585\x1B[0m\x1B[38;5;1m \x1B[38;5;68m\u2597\x1B[7m\x1B[38;5;61m\u2597\x1B[0m\x1B[38;5;1m \x1B[7m\x1B[38;5;25m\u258F\x1B[0m\x1B[38;5;23m\u258E\x1B[7m\x1B[38;5;103m\u258D\x1B[0m\x1B[38;5;1m \x1B[7m\x1B[38;5;68m\u2596\x1B[0m\x1B[38;5;68m\u2596 \x1B[7m\x1B[38;5;67m\u2585\x1B[0m\x1B[38;5;60m\u2584 \x1B[0m
|
|
425
|
+
\x1B[38;5;1m \x1B[38;5;60m\u259D\x1B[38;5;67m\u2598 \x1B[7m\x1B[38;5;24m\u258A\x1B[0m\x1B[38;5;24;48;5;25m\u2595\x1B[0m\x1B[38;5;24m \x1B[7m\x1B[38;5;68m\u2584\x1B[0m\x1B[38;5;1m \x1B[0m
|
|
426
|
+
\x1B[38;5;1m \x1B[7m\x1B[38;5;24m\u2585\x1B[0m\x1B[38;5;1m \x1B[0m
|
|
427
|
+
\x1B[?25h`
|
|
428
|
+
};
|
|
429
|
+
var LOGO_FULL = {
|
|
430
|
+
width: 19,
|
|
431
|
+
height: 9,
|
|
432
|
+
ansi: ` \x1B[38;5;1m \x1B[38;5;109m\u2597\x1B[38;5;67m\u2581\x1B[38;5;23m\u2584\u2581 \x1B[7m\x1B[38;5;68m\u2596\u259D\x1B[0m\x1B[38;5;1m \x1B[7m\x1B[38;5;110m\u258E\x1B[0m\x1B[38;5;1m \x1B[7m\x1B[38;5;25m\u258B\x1B[0m\x1B[38;5;237;48;5;25m\u2595\x1B[0m\x1B[38;5;237m \x1B[7m\x1B[38;5;110m\u2598\u2583\x1B[0m\x1B[38;5;60m\u258C\x1B[38;5;24m\u2598 \x1B[0m
|
|
433
|
+
\x1B[38;5;1m \x1B[7m\x1B[38;5;110m\u2585\x1B[38;5;67m\u259E\x1B[0m\x1B[38;5;110m\u2583\x1B[7m\x1B[38;5;23m\u259E\x1B[0m\x1B[38;5;24m\u2583 \x1B[7m\x1B[38;5;68m\u2596\u259D\x1B[38;5;103m\u258B\x1B[0m\x1B[38;5;109m\u258D\x1B[7m\x1B[38;5;25m\u258C\x1B[0m\x1B[38;5;25m\u258B \x1B[38;5;103m\u2584\x1B[7m\x1B[38;5;110m\u2597\x1B[38;5;61m\u259E\x1B[38;5;24m\u2583\x1B[0m\x1B[38;5;1m \x1B[38;5;60m\u2582\x1B[38;5;67m\u2584\x1B[38;5;60m\u2585\x1B[7m\u2584\x1B[0m\x1B[38;5;1m \x1B[0m
|
|
434
|
+
\x1B[38;5;1m \x1B[38;5;24m\u2597\x1B[38;5;25m\u2585\u2584\u2583\x1B[38;5;61m\u2581\x1B[7m\x1B[38;5;60m\u2586\x1B[38;5;103m\u2584\x1B[0m\x1B[38;5;67m\u2585\x1B[7m\x1B[38;5;60m\u2597\x1B[0m\x1B[38;5;24m\u2585\x1B[48;5;68m\u2596\x1B[0m\x1B[7m\x1B[38;5;68m\u259D\x1B[0m\x1B[38;5;103m\u258A\x1B[7m\x1B[38;5;25m\u258E\x1B[0m\x1B[38;5;68;48;5;24m\u2584\x1B[0m\x1B[38;5;110m\u259E\x1B[7m\x1B[38;5;67m\u259E\x1B[0m\x1B[38;5;25m\u259E\x1B[38;5;67m\u2581\x1B[38;5;68m\u2583\x1B[38;5;60m\u2586\x1B[7m\x1B[38;5;67m\u2583\x1B[38;5;68m\u2585\x1B[0m\x1B[38;5;61m\u2594 \x1B[0m
|
|
435
|
+
\x1B[38;5;1m \x1B[38;5;238m\u2594\x1B[38;5;23m\u2594\x1B[38;5;24m\u2594 \x1B[38;5;95m\u2582\x1B[38;5;131m\u2583\x1B[38;5;66m\u2594 \x1B[38;5;131m\u2582\u2583\x1B[38;5;60m\u2594\x1B[38;5;95m\u2582\x1B[38;5;131m\u2583\x1B[38;5;94m\u2596\x1B[38;5;95m\u2597 \x1B[38;5;60m\u2594\x1B[38;5;95m\u2583 \x1B[0m
|
|
436
|
+
\x1B[38;5;1m \x1B[7m\x1B[38;5;95m\u258B\x1B[0m\x1B[38;5;131m\u258C \x1B[38;5;95m\u2581\x1B[7m\x1B[38;5;131m\u258C\x1B[0m\x1B[38;5;131m\u258D \x1B[38;5;95m\u2581\x1B[7m\x1B[38;5;131m\u258D\x1B[0m\x1B[38;5;1m \x1B[7m\x1B[38;5;131m\u258D\x1B[38;5;95m\u2596\x1B[0m\x1B[38;5;131m\u2584\x1B[7m\u2596\x1B[0m\x1B[38;5;131m\u258E \x1B[0m
|
|
437
|
+
\x1B[38;5;1m \x1B[7m\x1B[38;5;95m\u2584\x1B[38;5;94m\u2583\x1B[38;5;95m\u2585\x1B[0m\x1B[38;5;1m \x1B[7m\x1B[38;5;95m\u2584\x1B[38;5;94m\u2584\x1B[0m\x1B[38;5;95m\u2598\x1B[38;5;167m\u259D\x1B[7m\x1B[38;5;95m\u2584\u2585\x1B[0m\x1B[38;5;131m\u259D \x1B[38;5;95m\u2594\x1B[38;5;238m\u259D\x1B[38;5;131m\u2598 \x1B[0m
|
|
438
|
+
\x1B[38;5;1m \x1B[38;5;60m\u2583\x1B[38;5;240m\u2596 \x1B[38;5;109m\u2582\x1B[38;5;110m\u2584\x1B[7m\x1B[38;5;67m\u2583\x1B[38;5;60m\u259E\x1B[0m\x1B[38;5;240;48;5;68m\u2594\x1B[0m\x1B[7m\x1B[38;5;67m\u2596\x1B[38;5;61m\u2597\x1B[0m\x1B[38;5;67;48;5;110m\u2594\x1B[0m\x1B[38;5;68m\u2585\x1B[7m\x1B[38;5;60m\u2597\x1B[0m\x1B[38;5;66m\u2585\x1B[38;5;60m\u2581\x1B[7m\x1B[38;5;25m\u2586\u2584\u2583\u2582\u2594\x1B[0m\x1B[38;5;25m\u2585\x1B[38;5;24m\u2596 \x1B[0m
|
|
439
|
+
\x1B[38;5;1m \x1B[38;5;60m\u2594\x1B[38;5;67m\u2582\x1B[38;5;110m\u2584\x1B[7m\u2594\u2584\x1B[0m\x1B[38;5;1m \x1B[38;5;61m\u2584\x1B[7m\x1B[38;5;67m\u2597\x1B[38;5;60m\u2597\x1B[38;5;25m\u258E\x1B[0m\x1B[38;5;25m\u258D\x1B[7m\x1B[38;5;110m\u259E\x1B[38;5;67m\u2596\x1B[0m\x1B[38;5;60;48;5;68m\u259D\x1B[0m\x1B[38;5;60m\u2598\x1B[7m\x1B[38;5;109m\u2585\x1B[0m\x1B[38;5;67m\u2585 \x1B[38;5;24m\u2594 \x1B[0m
|
|
440
|
+
\x1B[38;5;1m \x1B[38;5;66m\u2597\x1B[38;5;153m\u2586\x1B[7m\x1B[38;5;110m\u2582\u2585\x1B[0m\x1B[38;5;1m \x1B[7m\x1B[38;5;68m\u2598\x1B[38;5;61m\u2584\x1B[0m\x1B[38;5;1m \x1B[38;5;24;48;5;25m\u258F\x1B[0m\x1B[38;5;24m\u258E\x1B[7m\x1B[38;5;110m\u258B\x1B[0m\x1B[38;5;103m\u258D\x1B[7m\x1B[38;5;61m\u2596\x1B[0m\x1B[38;5;239;48;5;68m\u259D\x1B[0m\x1B[38;5;60m\u2596 \x1B[7m\x1B[38;5;109m\u2585\x1B[38;5;66m\u259D\x1B[0m\x1B[38;5;1m \x1B[0m
|
|
441
|
+
`
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// src/ui.ts
|
|
445
|
+
var theme = {
|
|
446
|
+
primary: chalk.hex("#89B4FA"),
|
|
447
|
+
// 亮蓝
|
|
448
|
+
accent: chalk.hex("#CBA6F7"),
|
|
449
|
+
// 亮紫
|
|
450
|
+
success: chalk.hex("#A6E3A1"),
|
|
451
|
+
// 亮绿
|
|
452
|
+
warning: chalk.hex("#F9E2AF"),
|
|
453
|
+
// 亮黄
|
|
454
|
+
danger: chalk.hex("#F38BA8"),
|
|
455
|
+
// 亮红
|
|
456
|
+
text: chalk.white,
|
|
457
|
+
muted: chalk.hex("#6C7086"),
|
|
458
|
+
// 中灰
|
|
459
|
+
dim: chalk.hex("#45475A")
|
|
460
|
+
// 暗灰
|
|
461
|
+
};
|
|
462
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
463
|
+
var spinnerIndex = 0;
|
|
464
|
+
var spinnerInterval = null;
|
|
465
|
+
var getSpinnerFrame = () => {
|
|
466
|
+
return theme.primary(SPINNER_FRAMES[spinnerIndex]);
|
|
467
|
+
};
|
|
468
|
+
var startSpinner = (onFrame) => {
|
|
469
|
+
if (spinnerInterval) return;
|
|
470
|
+
spinnerInterval = setInterval(() => {
|
|
471
|
+
spinnerIndex = (spinnerIndex + 1) % SPINNER_FRAMES.length;
|
|
472
|
+
onFrame();
|
|
473
|
+
}, 80);
|
|
474
|
+
};
|
|
475
|
+
var stopSpinner = () => {
|
|
476
|
+
if (spinnerInterval) {
|
|
477
|
+
clearInterval(spinnerInterval);
|
|
478
|
+
spinnerInterval = null;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
var getTerminalWidth = () => {
|
|
482
|
+
return process.stdout.columns || 80;
|
|
483
|
+
};
|
|
484
|
+
var selectLogo = (termWidth) => {
|
|
485
|
+
if (termWidth < 45) return LOGO_MINIMAL;
|
|
486
|
+
if (termWidth < 60) return LOGO_COMPACT;
|
|
487
|
+
return LOGO_FULL;
|
|
488
|
+
};
|
|
489
|
+
var renderLogoWithEnvPanel = (envName, env, defaultMode) => {
|
|
490
|
+
const termWidth = getTerminalWidth();
|
|
491
|
+
const isNarrow = termWidth < 60;
|
|
492
|
+
const isVeryNarrow = termWidth < 45;
|
|
493
|
+
const logo = selectLogo(termWidth);
|
|
494
|
+
const logoLines = logo.ansi.split("\n").filter((l) => l.length > 0);
|
|
495
|
+
const title = theme.primary("CCEM") + " " + theme.muted("Claude Code Env Manager");
|
|
496
|
+
const titleShort = theme.primary("CCEM");
|
|
497
|
+
const envLabel = theme.muted("Env: ") + theme.primary(envName);
|
|
498
|
+
const baseUrl = env.ANTHROPIC_BASE_URL || "-";
|
|
499
|
+
const model = env.ANTHROPIC_MODEL || "-";
|
|
500
|
+
const fastModel = env.ANTHROPIC_SMALL_FAST_MODEL || "-";
|
|
501
|
+
const apiKey = env.ANTHROPIC_API_KEY ? env.ANTHROPIC_API_KEY.slice(0, 2) + "\u2022\u2022\u2022\u2022" + env.ANTHROPIC_API_KEY.slice(-4) : "-";
|
|
502
|
+
const truncate = (s, max) => s.length > max ? s.slice(0, max - 3) + "..." : s;
|
|
503
|
+
const maskUrl = (url, max) => {
|
|
504
|
+
if (url.length <= max) return url;
|
|
505
|
+
try {
|
|
506
|
+
const parsed = new URL(url);
|
|
507
|
+
const protocol = parsed.protocol + "//";
|
|
508
|
+
const host = parsed.host;
|
|
509
|
+
const path6 = parsed.pathname + parsed.search;
|
|
510
|
+
const hostStart = host.slice(0, 8);
|
|
511
|
+
const hostEnd = host.slice(-4);
|
|
512
|
+
const pathPart = path6.length > 10 ? path6.slice(0, 7) + "..." : path6;
|
|
513
|
+
return `${protocol}${hostStart}...${hostEnd}${pathPart}`;
|
|
514
|
+
} catch {
|
|
515
|
+
return truncate(url, max);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
const labelWidth = 7;
|
|
519
|
+
let envLines;
|
|
520
|
+
if (isNarrow) {
|
|
521
|
+
envLines = [
|
|
522
|
+
envLabel,
|
|
523
|
+
theme.muted("Model:".padEnd(labelWidth)) + theme.dim(truncate(model, 25)),
|
|
524
|
+
theme.muted("Key:".padEnd(labelWidth)) + theme.dim(apiKey)
|
|
525
|
+
];
|
|
526
|
+
} else {
|
|
527
|
+
envLines = [
|
|
528
|
+
envLabel + (defaultMode && PERMISSION_PRESETS[defaultMode] ? " " + theme.accent(`[${PERMISSION_PRESETS[defaultMode].name}]`) : ""),
|
|
529
|
+
theme.muted("URL:".padEnd(labelWidth)) + theme.dim(maskUrl(baseUrl, 40)),
|
|
530
|
+
theme.muted("Model:".padEnd(labelWidth)) + theme.dim(truncate(model, 15)) + " " + theme.muted("Fast:".padEnd(labelWidth)) + theme.dim(truncate(fastModel, 15)),
|
|
531
|
+
theme.muted("Key:".padEnd(labelWidth)) + theme.dim(apiKey)
|
|
532
|
+
];
|
|
533
|
+
}
|
|
534
|
+
const lines = [];
|
|
535
|
+
if (isVeryNarrow) {
|
|
536
|
+
logoLines.forEach((line) => lines.push(line));
|
|
537
|
+
lines.push("");
|
|
538
|
+
lines.push(titleShort);
|
|
539
|
+
lines.push("");
|
|
540
|
+
envLines.forEach((line) => lines.push(line));
|
|
541
|
+
} else {
|
|
542
|
+
const gap = isNarrow ? " " : " ";
|
|
543
|
+
lines.push("");
|
|
544
|
+
const maxLines = Math.max(logoLines.length, envLines.length + 3);
|
|
545
|
+
for (let i = 0; i < maxLines; i++) {
|
|
546
|
+
const logoLine = logoLines[i] || "";
|
|
547
|
+
if (i === 0) {
|
|
548
|
+
const visibleWidth2 = logoLine.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "").length;
|
|
549
|
+
const padding2 = " ".repeat(Math.max(0, logo.width - visibleWidth2));
|
|
550
|
+
lines.push(logoLine + padding2);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (i === 1) {
|
|
554
|
+
const visibleWidth2 = logoLine.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "").length;
|
|
555
|
+
const padding2 = " ".repeat(Math.max(0, logo.width - visibleWidth2));
|
|
556
|
+
lines.push(logoLine + padding2 + gap + (isNarrow ? titleShort : title));
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (i === 2) {
|
|
560
|
+
const visibleWidth2 = logoLine.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "").length;
|
|
561
|
+
const padding2 = " ".repeat(Math.max(0, logo.width - visibleWidth2));
|
|
562
|
+
lines.push(logoLine + padding2);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const envLine = envLines[i - 3] || "";
|
|
566
|
+
const visibleWidth = logoLine.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "").length;
|
|
567
|
+
const padding = " ".repeat(Math.max(0, logo.width - visibleWidth));
|
|
568
|
+
lines.push(logoLine + padding + gap + envLine);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return lines.join("\n");
|
|
572
|
+
};
|
|
573
|
+
var renderUsageLine = (stats, loading) => {
|
|
574
|
+
if (loading) {
|
|
575
|
+
return theme.muted(" Usage: ") + getSpinnerFrame() + theme.dim(" Loading...");
|
|
576
|
+
}
|
|
577
|
+
if (!stats) {
|
|
578
|
+
return theme.muted(" Usage: ") + theme.dim("No data");
|
|
579
|
+
}
|
|
580
|
+
const termWidth = getTerminalWidth();
|
|
581
|
+
const isNarrow = termWidth < 70;
|
|
582
|
+
const todayTokens = getTotalTokens(stats.today);
|
|
583
|
+
const weekTokens = getTotalTokens(stats.week);
|
|
584
|
+
const totalTokens = getTotalTokens(stats.total);
|
|
585
|
+
if (isNarrow) {
|
|
586
|
+
return theme.muted(" Usage: ") + theme.text("Today ") + theme.primary(formatTokens(todayTokens)) + theme.dim(" | ") + theme.text("Week ") + theme.primary(formatTokens(weekTokens)) + theme.dim(" | ") + theme.text("Total ") + theme.primary(formatTokens(totalTokens));
|
|
587
|
+
}
|
|
588
|
+
return theme.muted(" Usage: ") + theme.text("Today ") + theme.primary(formatTokens(todayTokens).padStart(6)) + theme.dim(` (${formatCost(stats.today.cost)})`) + theme.dim(" | ") + theme.text("Week ") + theme.primary(formatTokens(weekTokens).padStart(6)) + theme.dim(` (${formatCost(stats.week.cost)})`) + theme.dim(" | ") + theme.text("Total ") + theme.primary(formatTokens(totalTokens).padStart(6)) + theme.dim(` (${formatCost(stats.total.cost)})`);
|
|
589
|
+
};
|
|
590
|
+
var renderCompactHeader = () => {
|
|
591
|
+
const width = process.stdout.columns || 80;
|
|
592
|
+
return theme.dim("\u2500".repeat(width));
|
|
593
|
+
};
|
|
594
|
+
var getMainMenuChoices = (defaultMode) => {
|
|
595
|
+
let startLabel = "Start Claude Code";
|
|
596
|
+
if (defaultMode && PERMISSION_PRESETS[defaultMode]) {
|
|
597
|
+
startLabel = `Start Claude Code ${theme.muted(`(${PERMISSION_PRESETS[defaultMode].name})`)}`;
|
|
598
|
+
}
|
|
599
|
+
return [
|
|
600
|
+
{ name: theme.success(startLabel), value: "start", short: "Start" },
|
|
601
|
+
{ name: theme.primary("Switch Environment"), value: "switch", short: "Switch" },
|
|
602
|
+
{ name: theme.primary("Permission Mode"), value: "perm", short: "Permission" },
|
|
603
|
+
{ name: theme.accent("View Usage"), value: "usage", short: "Usage" },
|
|
604
|
+
{ name: theme.text("Set Default Mode"), value: "setDefault", short: "Default" },
|
|
605
|
+
{ name: theme.muted("Exit"), value: "exit", short: "Exit" }
|
|
606
|
+
];
|
|
607
|
+
};
|
|
608
|
+
var modeColors = {
|
|
609
|
+
yolo: theme.danger,
|
|
610
|
+
dev: theme.success,
|
|
611
|
+
readonly: theme.primary,
|
|
612
|
+
safe: theme.warning,
|
|
613
|
+
ci: theme.accent,
|
|
614
|
+
audit: theme.primary
|
|
615
|
+
};
|
|
616
|
+
var getPermModeChoices = (currentMode, showCurrent = false) => {
|
|
617
|
+
const modes = ["yolo", "dev", "readonly", "safe", "ci", "audit"];
|
|
618
|
+
const choices = modes.map((mode) => {
|
|
619
|
+
const preset = PERMISSION_PRESETS[mode];
|
|
620
|
+
const colorFn = modeColors[mode];
|
|
621
|
+
const isCurrent = showCurrent && mode === currentMode;
|
|
622
|
+
const tag = isCurrent ? theme.success(" *") : "";
|
|
623
|
+
return {
|
|
624
|
+
name: colorFn(preset.name.padEnd(12)) + theme.muted(preset.description) + tag,
|
|
625
|
+
value: mode,
|
|
626
|
+
short: preset.name
|
|
627
|
+
};
|
|
628
|
+
});
|
|
629
|
+
if (showCurrent) {
|
|
630
|
+
choices.push({
|
|
631
|
+
name: theme.muted("Clear default"),
|
|
632
|
+
value: "clear",
|
|
633
|
+
short: "Clear"
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
choices.push({
|
|
637
|
+
name: theme.muted("Back"),
|
|
638
|
+
value: "back",
|
|
639
|
+
short: "Back"
|
|
640
|
+
});
|
|
641
|
+
return choices;
|
|
642
|
+
};
|
|
643
|
+
var msg = {
|
|
644
|
+
success: (text) => console.log(theme.success("\u2713 ") + theme.text(text)),
|
|
645
|
+
error: (text) => console.log(theme.danger("\u2717 ") + theme.text(text)),
|
|
646
|
+
warning: (text) => console.log(theme.warning("! ") + theme.text(text)),
|
|
647
|
+
info: (text) => console.log(theme.primary("\u203A ") + theme.text(text))
|
|
648
|
+
};
|
|
649
|
+
var renderStarting = () => {
|
|
650
|
+
return theme.muted("Starting Claude Code...");
|
|
651
|
+
};
|
|
652
|
+
var renderCalendarHeatmap = (stats, months = 6) => {
|
|
653
|
+
const lines = [];
|
|
654
|
+
const now = /* @__PURE__ */ new Date();
|
|
655
|
+
const startDate = new Date(now);
|
|
656
|
+
startDate.setMonth(startDate.getMonth() - months);
|
|
657
|
+
const day = startDate.getDay();
|
|
658
|
+
const diff = startDate.getDate() - day + (day === 0 ? -6 : 1);
|
|
659
|
+
startDate.setDate(diff);
|
|
660
|
+
startDate.setHours(0, 0, 0, 0);
|
|
661
|
+
const dates = [];
|
|
662
|
+
const d = new Date(startDate);
|
|
663
|
+
while (d <= now || d.getDay() !== 1) {
|
|
664
|
+
dates.push(d.toISOString().split("T")[0]);
|
|
665
|
+
d.setDate(d.getDate() + 1);
|
|
666
|
+
if (d > now && d.getDay() === 1) break;
|
|
667
|
+
}
|
|
668
|
+
let maxTokens = 0;
|
|
669
|
+
for (const date of dates) {
|
|
670
|
+
const usage = stats.dailyHistory[date];
|
|
671
|
+
if (usage) {
|
|
672
|
+
const tokens = getTotalTokens(usage);
|
|
673
|
+
if (tokens > maxTokens) maxTokens = tokens;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
let header = " ";
|
|
677
|
+
let currentMonth = -1;
|
|
678
|
+
const weeks = Math.ceil(dates.length / 7);
|
|
679
|
+
for (let w = 0; w < weeks; w++) {
|
|
680
|
+
const dateIndex = w * 7;
|
|
681
|
+
if (dateIndex < dates.length) {
|
|
682
|
+
const date = new Date(dates[dateIndex]);
|
|
683
|
+
const month = date.getMonth();
|
|
684
|
+
if (month !== currentMonth && w < weeks - 1) {
|
|
685
|
+
const monthName = date.toLocaleString("default", { month: "short" });
|
|
686
|
+
header += theme.muted(monthName);
|
|
687
|
+
currentMonth = month;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
header = " ";
|
|
692
|
+
let lastMonth = -1;
|
|
693
|
+
for (let w = 0; w < weeks; w++) {
|
|
694
|
+
const dateIndex = w * 7;
|
|
695
|
+
if (dateIndex >= dates.length) break;
|
|
696
|
+
const date = new Date(dates[dateIndex]);
|
|
697
|
+
const month = date.getMonth();
|
|
698
|
+
if (month !== lastMonth) {
|
|
699
|
+
const monthName = date.toLocaleString("default", { month: "short" });
|
|
700
|
+
header += theme.muted(monthName.padEnd(4));
|
|
701
|
+
w++;
|
|
702
|
+
lastMonth = month;
|
|
703
|
+
} else {
|
|
704
|
+
header += " ";
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
lines.push(header);
|
|
708
|
+
const dayLabels = ["Mon", "", "Wed", "", "Fri", "", "Sun"];
|
|
709
|
+
const levels = [" ", "\u2591", "\u2592", "\u2593", "\u2588"];
|
|
710
|
+
for (let dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) {
|
|
711
|
+
let row = theme.muted((dayLabels[dayOfWeek] || "").padEnd(4) + " ");
|
|
712
|
+
for (let w = 0; w < weeks; w++) {
|
|
713
|
+
const dateIndex = w * 7 + dayOfWeek;
|
|
714
|
+
if (dateIndex < dates.length) {
|
|
715
|
+
const dateKey = dates[dateIndex];
|
|
716
|
+
if (new Date(dateKey) > now) {
|
|
717
|
+
row += " ";
|
|
718
|
+
} else {
|
|
719
|
+
const usage = stats.dailyHistory[dateKey];
|
|
720
|
+
const tokens = usage ? getTotalTokens(usage) : 0;
|
|
721
|
+
let level = 0;
|
|
722
|
+
if (tokens > 0) {
|
|
723
|
+
if (maxTokens === 0) level = 0;
|
|
724
|
+
else {
|
|
725
|
+
level = Math.ceil(tokens / maxTokens * 4);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
row += (level === 0 ? theme.dim("\xB7") : theme.primary(levels[level])) + " ";
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
lines.push(row);
|
|
733
|
+
}
|
|
734
|
+
lines.push("");
|
|
735
|
+
lines.push(" " + theme.dim("Less ") + theme.dim("\xB7") + " " + theme.primary(levels[1]) + " " + theme.primary(levels[2]) + " " + theme.primary(levels[3]) + " " + theme.primary(levels[4]) + " " + theme.dim(" More"));
|
|
736
|
+
return lines.join("\n");
|
|
737
|
+
};
|
|
738
|
+
var renderUsageDetail = (stats) => {
|
|
739
|
+
const lines = [];
|
|
740
|
+
lines.push("");
|
|
741
|
+
lines.push(theme.primary(" Token Usage Statistics"));
|
|
742
|
+
lines.push(theme.dim("\u2500".repeat(60)));
|
|
743
|
+
lines.push("");
|
|
744
|
+
lines.push(renderCalendarHeatmap(stats));
|
|
745
|
+
lines.push("");
|
|
746
|
+
lines.push(theme.dim("\u2500".repeat(60)));
|
|
747
|
+
const periodTable = new Table({
|
|
748
|
+
head: [
|
|
749
|
+
theme.muted("Period"),
|
|
750
|
+
theme.muted("Input"),
|
|
751
|
+
theme.muted("Output"),
|
|
752
|
+
theme.muted("Cache Read"),
|
|
753
|
+
theme.muted("Cost")
|
|
754
|
+
],
|
|
755
|
+
style: { head: [], border: [] },
|
|
756
|
+
chars: {
|
|
757
|
+
"top": "",
|
|
758
|
+
"top-mid": "",
|
|
759
|
+
"top-left": "",
|
|
760
|
+
"top-right": "",
|
|
761
|
+
"bottom": "",
|
|
762
|
+
"bottom-mid": "",
|
|
763
|
+
"bottom-left": "",
|
|
764
|
+
"bottom-right": "",
|
|
765
|
+
"left": " ",
|
|
766
|
+
"left-mid": "",
|
|
767
|
+
"mid": "",
|
|
768
|
+
"mid-mid": "",
|
|
769
|
+
"right": "",
|
|
770
|
+
"right-mid": "",
|
|
771
|
+
"middle": " "
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
const formatRow = (label, usage) => [
|
|
775
|
+
theme.text(label),
|
|
776
|
+
theme.primary(formatTokens(usage.inputTokens)),
|
|
777
|
+
theme.primary(formatTokens(usage.outputTokens)),
|
|
778
|
+
theme.primary(formatTokens(usage.cacheReadTokens)),
|
|
779
|
+
theme.success(formatCost(usage.cost))
|
|
780
|
+
];
|
|
781
|
+
periodTable.push(formatRow("Today", stats.today));
|
|
782
|
+
periodTable.push(formatRow("This Week", stats.week));
|
|
783
|
+
periodTable.push(formatRow("All Time", stats.total));
|
|
784
|
+
lines.push(periodTable.toString());
|
|
785
|
+
const modelEntries = Object.entries(stats.byModel).sort((a, b) => b[1].cost - a[1].cost);
|
|
786
|
+
if (modelEntries.length > 0) {
|
|
787
|
+
lines.push("");
|
|
788
|
+
lines.push(theme.dim("\u2500".repeat(60)));
|
|
789
|
+
lines.push(theme.muted(" By Model"));
|
|
790
|
+
lines.push("");
|
|
791
|
+
const modelTable = new Table({
|
|
792
|
+
head: [
|
|
793
|
+
theme.muted("Model"),
|
|
794
|
+
theme.muted("Tokens"),
|
|
795
|
+
theme.muted("Cost")
|
|
796
|
+
],
|
|
797
|
+
style: { head: [], border: [] },
|
|
798
|
+
chars: {
|
|
799
|
+
"top": "",
|
|
800
|
+
"top-mid": "",
|
|
801
|
+
"top-left": "",
|
|
802
|
+
"top-right": "",
|
|
803
|
+
"bottom": "",
|
|
804
|
+
"bottom-mid": "",
|
|
805
|
+
"bottom-left": "",
|
|
806
|
+
"bottom-right": "",
|
|
807
|
+
"left": " ",
|
|
808
|
+
"left-mid": "",
|
|
809
|
+
"mid": "",
|
|
810
|
+
"mid-mid": "",
|
|
811
|
+
"right": "",
|
|
812
|
+
"right-mid": "",
|
|
813
|
+
"middle": " "
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
for (const [model, usage] of modelEntries) {
|
|
817
|
+
const totalTokens = getTotalTokens(usage);
|
|
818
|
+
modelTable.push([
|
|
819
|
+
theme.text(model),
|
|
820
|
+
theme.primary(formatTokens(totalTokens)),
|
|
821
|
+
theme.success(formatCost(usage.cost))
|
|
822
|
+
]);
|
|
823
|
+
}
|
|
824
|
+
lines.push(modelTable.toString());
|
|
825
|
+
}
|
|
826
|
+
lines.push("");
|
|
827
|
+
lines.push(theme.dim("\u2500".repeat(60)));
|
|
828
|
+
lines.push(theme.muted(` Last updated: ${new Date(stats.lastUpdated).toLocaleString()}`));
|
|
829
|
+
return lines.join("\n");
|
|
830
|
+
};
|
|
831
|
+
var renderFooterHints = (hints) => {
|
|
832
|
+
const termWidth = process.stdout.columns || 80;
|
|
833
|
+
const full = hints.map((h) => theme.primary(h.key) + theme.dim(h.label)).join(" ");
|
|
834
|
+
const medium = hints.map((h) => theme.primary(h.key) + theme.dim(h.short || h.label)).join(" ");
|
|
835
|
+
const minimal = hints.map((h) => theme.primary(h.key)).join(" ");
|
|
836
|
+
const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*m/g, "");
|
|
837
|
+
if (stripAnsi(full).length <= termWidth) return full;
|
|
838
|
+
if (stripAnsi(medium).length <= termWidth) return medium;
|
|
839
|
+
return minimal;
|
|
840
|
+
};
|
|
841
|
+
var selectEnvWithKeys = (registries, current) => {
|
|
842
|
+
return new Promise((resolve2) => {
|
|
843
|
+
const envNames = Object.keys(registries);
|
|
844
|
+
let selectedIndex = envNames.indexOf(current);
|
|
845
|
+
if (selectedIndex === -1) selectedIndex = 0;
|
|
846
|
+
const stdin = process.stdin;
|
|
847
|
+
const wasRaw = stdin.isRaw;
|
|
848
|
+
stdin.setRawMode(true);
|
|
849
|
+
stdin.resume();
|
|
850
|
+
const footerHints = [
|
|
851
|
+
{ key: "[\u2191\u2193]", label: " navigate", short: " nav" },
|
|
852
|
+
{ key: "[Enter]", label: " select", short: " sel" },
|
|
853
|
+
{ key: "[e]", label: "dit", short: "" },
|
|
854
|
+
{ key: "[r]", label: "ename", short: "en" },
|
|
855
|
+
{ key: "[c]", label: "opy", short: "py" },
|
|
856
|
+
{ key: "[d]", label: "elete", short: "el" }
|
|
857
|
+
];
|
|
858
|
+
const totalLines = envNames.length + 1;
|
|
859
|
+
const renderItem = (index) => {
|
|
860
|
+
const name = envNames[index];
|
|
861
|
+
const isCurrent = name === current;
|
|
862
|
+
const isSelected = index === selectedIndex;
|
|
863
|
+
const prefix = isSelected ? theme.primary("\u276F ") : " ";
|
|
864
|
+
const tag = isCurrent ? theme.success(" *") : "";
|
|
865
|
+
const nameText = isSelected ? theme.primary(name) : isCurrent ? theme.primary(name) : theme.text(name);
|
|
866
|
+
return prefix + nameText + tag;
|
|
867
|
+
};
|
|
868
|
+
const updateLine = (lineIndex, content) => {
|
|
869
|
+
const moveUp = totalLines - lineIndex;
|
|
870
|
+
process.stdout.write(`\x1B[${moveUp}A`);
|
|
871
|
+
process.stdout.write("\x1B[2K");
|
|
872
|
+
process.stdout.write("\x1B[G");
|
|
873
|
+
process.stdout.write(content);
|
|
874
|
+
process.stdout.write(`\x1B[${moveUp}B`);
|
|
875
|
+
process.stdout.write("\x1B[G");
|
|
876
|
+
};
|
|
877
|
+
const initialRender = () => {
|
|
878
|
+
process.stdout.write("\x1B[?25l");
|
|
879
|
+
envNames.forEach((_, i) => {
|
|
880
|
+
console.log(renderItem(i));
|
|
881
|
+
});
|
|
882
|
+
console.log(renderFooterHints(footerHints));
|
|
883
|
+
};
|
|
884
|
+
const handleSelectionChange = (oldIndex, newIndex) => {
|
|
885
|
+
if (oldIndex === newIndex) return;
|
|
886
|
+
selectedIndex = newIndex;
|
|
887
|
+
updateLine(oldIndex, renderItem(oldIndex));
|
|
888
|
+
updateLine(newIndex, renderItem(newIndex));
|
|
889
|
+
};
|
|
890
|
+
initialRender();
|
|
891
|
+
const cleanup = () => {
|
|
892
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
893
|
+
stdin.removeListener("data", onKeypress);
|
|
894
|
+
process.stdout.write("\x1B[?25h");
|
|
895
|
+
};
|
|
896
|
+
const onKeypress = (key) => {
|
|
897
|
+
const char = key.toString();
|
|
898
|
+
if (char === "") {
|
|
899
|
+
cleanup();
|
|
900
|
+
process.exit(0);
|
|
901
|
+
}
|
|
902
|
+
if (char === "\x1B" && key.length === 1) {
|
|
903
|
+
cleanup();
|
|
904
|
+
resolve2({ action: "cancel" });
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (char === "\x1B[A" || char === "k") {
|
|
908
|
+
const oldIndex = selectedIndex;
|
|
909
|
+
const newIndex = Math.max(0, selectedIndex - 1);
|
|
910
|
+
handleSelectionChange(oldIndex, newIndex);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (char === "\x1B[B" || char === "j") {
|
|
914
|
+
const oldIndex = selectedIndex;
|
|
915
|
+
const newIndex = Math.min(envNames.length - 1, selectedIndex + 1);
|
|
916
|
+
handleSelectionChange(oldIndex, newIndex);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (char === "\r" || char === "\n") {
|
|
920
|
+
cleanup();
|
|
921
|
+
resolve2({ action: "select", name: envNames[selectedIndex] });
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (char === "e" || char === "E") {
|
|
925
|
+
cleanup();
|
|
926
|
+
resolve2({ action: "edit", name: envNames[selectedIndex] });
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (char === "r" || char === "R") {
|
|
930
|
+
cleanup();
|
|
931
|
+
resolve2({ action: "rename", name: envNames[selectedIndex] });
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (char === "c" || char === "C") {
|
|
935
|
+
cleanup();
|
|
936
|
+
resolve2({ action: "copy", name: envNames[selectedIndex] });
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (char === "d" || char === "D") {
|
|
940
|
+
cleanup();
|
|
941
|
+
resolve2({ action: "delete", name: envNames[selectedIndex] });
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
stdin.on("data", onKeypress);
|
|
946
|
+
});
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
// src/permissions.ts
|
|
950
|
+
import fs4 from "fs";
|
|
951
|
+
import chalk3 from "chalk";
|
|
952
|
+
import Table2 from "cli-table3";
|
|
953
|
+
import { PERMISSION_PRESETS as PERMISSION_PRESETS3, getPermissionModeNames } from "@ccem/core";
|
|
954
|
+
|
|
955
|
+
// src/utils.ts
|
|
956
|
+
import crypto from "crypto";
|
|
957
|
+
import fs2 from "fs";
|
|
958
|
+
import path2 from "path";
|
|
959
|
+
var SECRET_KEY = crypto.scryptSync("claude-code-env-manager-secret", "salt", 32);
|
|
960
|
+
var findProjectRoot = () => {
|
|
961
|
+
let currentDir = process.cwd();
|
|
962
|
+
const root = path2.parse(currentDir).root;
|
|
963
|
+
while (currentDir !== root) {
|
|
964
|
+
if (fs2.existsSync(path2.join(currentDir, ".git")) || fs2.existsSync(path2.join(currentDir, "package.json"))) {
|
|
965
|
+
return currentDir;
|
|
966
|
+
}
|
|
967
|
+
currentDir = path2.dirname(currentDir);
|
|
968
|
+
}
|
|
969
|
+
return process.cwd();
|
|
970
|
+
};
|
|
971
|
+
var getSettingsPath = (useLocal = true) => {
|
|
972
|
+
const projectRoot = findProjectRoot();
|
|
973
|
+
const claudeDir = path2.join(projectRoot, ".claude");
|
|
974
|
+
const filename = useLocal ? "settings.local.json" : "settings.json";
|
|
975
|
+
return path2.join(claudeDir, filename);
|
|
976
|
+
};
|
|
977
|
+
var ensureClaudeDir = () => {
|
|
978
|
+
const projectRoot = findProjectRoot();
|
|
979
|
+
const claudeDir = path2.join(projectRoot, ".claude");
|
|
980
|
+
if (!fs2.existsSync(claudeDir)) {
|
|
981
|
+
fs2.mkdirSync(claudeDir, { recursive: true });
|
|
982
|
+
}
|
|
983
|
+
return claudeDir;
|
|
984
|
+
};
|
|
985
|
+
var getHomeDir = () => {
|
|
986
|
+
return process.env.HOME || process.env.USERPROFILE || "";
|
|
987
|
+
};
|
|
988
|
+
var getGlobalClaudeConfigPath = () => {
|
|
989
|
+
return path2.join(getHomeDir(), ".claude.json");
|
|
990
|
+
};
|
|
991
|
+
var getGlobalClaudeSettingsPath = () => {
|
|
992
|
+
return path2.join(getHomeDir(), ".claude", "settings.json");
|
|
993
|
+
};
|
|
994
|
+
var ensureGlobalClaudeDir = () => {
|
|
995
|
+
const claudeDir = path2.join(getHomeDir(), ".claude");
|
|
996
|
+
if (!fs2.existsSync(claudeDir)) {
|
|
997
|
+
fs2.mkdirSync(claudeDir, { recursive: true });
|
|
998
|
+
}
|
|
999
|
+
return claudeDir;
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
// src/launcher.ts
|
|
1003
|
+
import { spawn } from "child_process";
|
|
1004
|
+
import * as fs3 from "fs";
|
|
1005
|
+
import * as path3 from "path";
|
|
1006
|
+
import chalk2 from "chalk";
|
|
1007
|
+
import { decrypt, PERMISSION_PRESETS as PERMISSION_PRESETS2, ensureCcemDir as ensureCcemDir2 } from "@ccem/core";
|
|
1008
|
+
function buildEnvVars(envConfig) {
|
|
1009
|
+
const vars = {};
|
|
1010
|
+
if (envConfig.ANTHROPIC_BASE_URL) vars.ANTHROPIC_BASE_URL = envConfig.ANTHROPIC_BASE_URL;
|
|
1011
|
+
if (envConfig.ANTHROPIC_API_KEY) vars.ANTHROPIC_API_KEY = decrypt(envConfig.ANTHROPIC_API_KEY);
|
|
1012
|
+
if (envConfig.ANTHROPIC_MODEL) vars.ANTHROPIC_MODEL = envConfig.ANTHROPIC_MODEL;
|
|
1013
|
+
if (envConfig.ANTHROPIC_SMALL_FAST_MODEL) vars.ANTHROPIC_SMALL_FAST_MODEL = envConfig.ANTHROPIC_SMALL_FAST_MODEL;
|
|
1014
|
+
return vars;
|
|
1015
|
+
}
|
|
1016
|
+
function buildPermArgs(modeName) {
|
|
1017
|
+
const preset = PERMISSION_PRESETS2[modeName];
|
|
1018
|
+
if (!preset) return [];
|
|
1019
|
+
const args = ["--permission-mode", preset.permissionMode];
|
|
1020
|
+
if (preset.permissions.allow.length > 0) {
|
|
1021
|
+
const quoted = preset.permissions.allow.map((t) => `"${t}"`).join(" ");
|
|
1022
|
+
args.push("--allowedTools", quoted);
|
|
1023
|
+
}
|
|
1024
|
+
if (preset.permissions.deny.length > 0) {
|
|
1025
|
+
const quoted = preset.permissions.deny.map((t) => `"${t}"`).join(" ");
|
|
1026
|
+
args.push("--disallowedTools", quoted);
|
|
1027
|
+
}
|
|
1028
|
+
return args;
|
|
1029
|
+
}
|
|
1030
|
+
function ensureSessionsDir() {
|
|
1031
|
+
const dir = path3.join(ensureCcemDir2(), "sessions");
|
|
1032
|
+
if (!fs3.existsSync(dir)) {
|
|
1033
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1034
|
+
}
|
|
1035
|
+
return dir;
|
|
1036
|
+
}
|
|
1037
|
+
async function launchClaude(options) {
|
|
1038
|
+
const { envConfig, permMode, workingDir, sessionId, resumeSessionId, silent } = options;
|
|
1039
|
+
const env = { ...process.env };
|
|
1040
|
+
if (envConfig) {
|
|
1041
|
+
Object.assign(env, buildEnvVars(envConfig));
|
|
1042
|
+
}
|
|
1043
|
+
delete env.CLAUDECODE;
|
|
1044
|
+
const args = [];
|
|
1045
|
+
if (permMode) {
|
|
1046
|
+
const preset = PERMISSION_PRESETS2[permMode];
|
|
1047
|
+
if (preset) {
|
|
1048
|
+
if (!silent) {
|
|
1049
|
+
console.log(chalk2.green(`\u5DF2\u5E94\u7528 ${preset.name}\uFF08\u4E34\u65F6\uFF09`));
|
|
1050
|
+
console.log(chalk2.gray(`\u8BF4\u660E: ${preset.description}`));
|
|
1051
|
+
console.log("");
|
|
1052
|
+
}
|
|
1053
|
+
args.push(...buildPermArgs(permMode));
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (resumeSessionId) {
|
|
1057
|
+
args.push("--resume", resumeSessionId);
|
|
1058
|
+
}
|
|
1059
|
+
if (workingDir) {
|
|
1060
|
+
process.chdir(workingDir);
|
|
1061
|
+
}
|
|
1062
|
+
if (!silent && !permMode) {
|
|
1063
|
+
console.log(renderStarting());
|
|
1064
|
+
}
|
|
1065
|
+
const sessionsDir = ensureSessionsDir();
|
|
1066
|
+
return new Promise((resolve2) => {
|
|
1067
|
+
const child = spawn("claude", args, {
|
|
1068
|
+
stdio: "inherit",
|
|
1069
|
+
shell: true,
|
|
1070
|
+
env
|
|
1071
|
+
});
|
|
1072
|
+
child.on("exit", (code) => {
|
|
1073
|
+
if (sessionId) {
|
|
1074
|
+
try {
|
|
1075
|
+
fs3.writeFileSync(
|
|
1076
|
+
path3.join(sessionsDir, `${sessionId}.exit`),
|
|
1077
|
+
String(code ?? 0)
|
|
1078
|
+
);
|
|
1079
|
+
} catch {
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
process.exit(code ?? 0);
|
|
1083
|
+
});
|
|
1084
|
+
child.on("error", (err) => {
|
|
1085
|
+
console.error(chalk2.red(`\u542F\u52A8 Claude Code \u5931\u8D25: ${err.message}`));
|
|
1086
|
+
process.exit(1);
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// src/permissions.ts
|
|
1092
|
+
var readSettings = (settingsPath) => {
|
|
1093
|
+
if (fs4.existsSync(settingsPath)) {
|
|
1094
|
+
try {
|
|
1095
|
+
const content = fs4.readFileSync(settingsPath, "utf-8");
|
|
1096
|
+
return JSON.parse(content);
|
|
1097
|
+
} catch {
|
|
1098
|
+
console.warn(chalk3.yellow(`\u8B66\u544A: \u65E0\u6CD5\u89E3\u6790 ${settingsPath}\uFF0C\u5C06\u521B\u5EFA\u5907\u4EFD`));
|
|
1099
|
+
const backupPath = settingsPath + ".error." + Date.now();
|
|
1100
|
+
fs4.copyFileSync(settingsPath, backupPath);
|
|
1101
|
+
console.log(chalk3.gray(`\u5907\u4EFD\u5DF2\u4FDD\u5B58\u5230: ${backupPath}`));
|
|
1102
|
+
return {};
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return {};
|
|
1106
|
+
};
|
|
1107
|
+
var writeSettings = (settingsPath, config3) => {
|
|
1108
|
+
ensureClaudeDir();
|
|
1109
|
+
fs4.writeFileSync(settingsPath, JSON.stringify(config3, null, 2) + "\n");
|
|
1110
|
+
};
|
|
1111
|
+
var mergePermissions = (existing, preset) => {
|
|
1112
|
+
const existingAllow = existing.permissions?.allow || [];
|
|
1113
|
+
const existingDeny = existing.permissions?.deny || [];
|
|
1114
|
+
const mergedAllow = [.../* @__PURE__ */ new Set([...preset.allow, ...existingAllow])];
|
|
1115
|
+
const mergedDeny = [.../* @__PURE__ */ new Set([...preset.deny, ...existingDeny])];
|
|
1116
|
+
return {
|
|
1117
|
+
...existing,
|
|
1118
|
+
permissions: {
|
|
1119
|
+
allow: mergedAllow,
|
|
1120
|
+
deny: mergedDeny
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
};
|
|
1124
|
+
var applyPermissionMode = (modeName) => {
|
|
1125
|
+
const preset = PERMISSION_PRESETS3[modeName];
|
|
1126
|
+
if (!preset) {
|
|
1127
|
+
console.error(chalk3.red(`\u672A\u77E5\u7684\u6743\u9650\u6A21\u5F0F: ${modeName}`));
|
|
1128
|
+
console.log(chalk3.yellow("\u53EF\u7528\u6A21\u5F0F: " + getPermissionModeNames().join(", ")));
|
|
1129
|
+
process.exit(1);
|
|
1130
|
+
}
|
|
1131
|
+
const settingsPath = getSettingsPath(true);
|
|
1132
|
+
const existing = readSettings(settingsPath);
|
|
1133
|
+
const merged = mergePermissions(existing, preset.permissions);
|
|
1134
|
+
writeSettings(settingsPath, merged);
|
|
1135
|
+
console.log(chalk3.green(`\u5DF2\u5E94\u7528 ${preset.name}`));
|
|
1136
|
+
console.log(chalk3.gray(`\u914D\u7F6E\u5DF2\u5199\u5165: ${settingsPath}`));
|
|
1137
|
+
console.log(chalk3.gray(`\u8BF4\u660E: ${preset.description}`));
|
|
1138
|
+
};
|
|
1139
|
+
var resetPermissions = () => {
|
|
1140
|
+
const settingsPath = getSettingsPath(true);
|
|
1141
|
+
if (!fs4.existsSync(settingsPath)) {
|
|
1142
|
+
console.log(chalk3.yellow("\u6CA1\u6709\u81EA\u5B9A\u4E49\u6743\u9650\u914D\u7F6E\u9700\u8981\u91CD\u7F6E"));
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
const config3 = readSettings(settingsPath);
|
|
1146
|
+
delete config3.permissions;
|
|
1147
|
+
if (Object.keys(config3).length === 0) {
|
|
1148
|
+
fs4.unlinkSync(settingsPath);
|
|
1149
|
+
console.log(chalk3.green("\u5DF2\u5220\u9664\u914D\u7F6E\u6587\u4EF6\uFF08\u6587\u4EF6\u4E3A\u7A7A\uFF09"));
|
|
1150
|
+
} else {
|
|
1151
|
+
writeSettings(settingsPath, config3);
|
|
1152
|
+
console.log(chalk3.green("\u6743\u9650\u914D\u7F6E\u5DF2\u91CD\u7F6E"));
|
|
1153
|
+
}
|
|
1154
|
+
console.log(chalk3.gray(`\u6587\u4EF6: ${settingsPath}`));
|
|
1155
|
+
};
|
|
1156
|
+
var showCurrentMode = () => {
|
|
1157
|
+
const settingsPath = getSettingsPath(true);
|
|
1158
|
+
if (!fs4.existsSync(settingsPath)) {
|
|
1159
|
+
console.log(chalk3.yellow("\u672A\u914D\u7F6E\u81EA\u5B9A\u4E49\u6743\u9650"));
|
|
1160
|
+
console.log(chalk3.gray(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${settingsPath}`));
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
const config3 = readSettings(settingsPath);
|
|
1164
|
+
if (!config3.permissions) {
|
|
1165
|
+
console.log(chalk3.yellow("\u672A\u914D\u7F6E\u81EA\u5B9A\u4E49\u6743\u9650"));
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const matchedPreset = Object.entries(PERMISSION_PRESETS3).find(([_, preset]) => {
|
|
1169
|
+
const configAllow = new Set(config3.permissions?.allow || []);
|
|
1170
|
+
const configDeny = new Set(config3.permissions?.deny || []);
|
|
1171
|
+
const presetAllow = new Set(preset.permissions.allow);
|
|
1172
|
+
const presetDeny = new Set(preset.permissions.deny);
|
|
1173
|
+
const allowMatch = preset.permissions.allow.every((p) => configAllow.has(p));
|
|
1174
|
+
const denyMatch = preset.permissions.deny.every((p) => configDeny.has(p));
|
|
1175
|
+
return allowMatch && denyMatch;
|
|
1176
|
+
});
|
|
1177
|
+
if (matchedPreset) {
|
|
1178
|
+
console.log(chalk3.green(`\u5F53\u524D\u6A21\u5F0F: ${matchedPreset[0]} (${matchedPreset[1].name})`));
|
|
1179
|
+
} else {
|
|
1180
|
+
console.log(chalk3.yellow("\u5F53\u524D\u6A21\u5F0F: \u81EA\u5B9A\u4E49"));
|
|
1181
|
+
}
|
|
1182
|
+
console.log(chalk3.gray(`\u914D\u7F6E\u6587\u4EF6: ${settingsPath}`));
|
|
1183
|
+
const table = new Table2({
|
|
1184
|
+
head: ["\u7C7B\u578B", "\u89C4\u5219"],
|
|
1185
|
+
style: { head: ["cyan"] },
|
|
1186
|
+
colWidths: [10, 70]
|
|
1187
|
+
});
|
|
1188
|
+
const allowRules = config3.permissions.allow || [];
|
|
1189
|
+
const denyRules = config3.permissions.deny || [];
|
|
1190
|
+
table.push(["Allow", allowRules.length > 0 ? allowRules.join("\n") : "(\u65E0)"]);
|
|
1191
|
+
table.push(["Deny", denyRules.length > 0 ? denyRules.join("\n") : "(\u65E0)"]);
|
|
1192
|
+
console.log(table.toString());
|
|
1193
|
+
};
|
|
1194
|
+
var listAvailableModes = () => {
|
|
1195
|
+
const table = new Table2({
|
|
1196
|
+
head: ["\u6A21\u5F0F", "\u6807\u5FD7", "\u8BF4\u660E"],
|
|
1197
|
+
style: { head: ["cyan"] },
|
|
1198
|
+
colWidths: [15, 15, 50]
|
|
1199
|
+
});
|
|
1200
|
+
Object.entries(PERMISSION_PRESETS3).forEach(([key, preset]) => {
|
|
1201
|
+
table.push([preset.name, `--${key}`, preset.description]);
|
|
1202
|
+
});
|
|
1203
|
+
console.log(chalk3.bold("\u53EF\u7528\u6743\u9650\u6A21\u5F0F:\n"));
|
|
1204
|
+
console.log(table.toString());
|
|
1205
|
+
console.log(chalk3.gray("\n\u4E34\u65F6\u6A21\u5F0F: ccem <mode>"));
|
|
1206
|
+
console.log(chalk3.gray("\u6C38\u4E45\u6A21\u5F0F: ccem setup perms --<mode>"));
|
|
1207
|
+
};
|
|
1208
|
+
var runWithTempPermissions = async (modeName, envConfig) => {
|
|
1209
|
+
const preset = PERMISSION_PRESETS3[modeName];
|
|
1210
|
+
if (!preset) {
|
|
1211
|
+
console.error(chalk3.red(`\u672A\u77E5\u7684\u6743\u9650\u6A21\u5F0F: ${modeName}`));
|
|
1212
|
+
console.log(chalk3.yellow("\u53EF\u7528\u6A21\u5F0F: " + getPermissionModeNames().join(", ")));
|
|
1213
|
+
process.exit(1);
|
|
1214
|
+
}
|
|
1215
|
+
await launchClaude({ envConfig, permMode: modeName });
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
// src/setup.ts
|
|
1219
|
+
import fs5 from "fs";
|
|
1220
|
+
import chalk4 from "chalk";
|
|
1221
|
+
import { spawn as spawn2 } from "child_process";
|
|
1222
|
+
var readJsonFile = (filePath) => {
|
|
1223
|
+
if (fs5.existsSync(filePath)) {
|
|
1224
|
+
try {
|
|
1225
|
+
const content = fs5.readFileSync(filePath, "utf-8");
|
|
1226
|
+
return JSON.parse(content);
|
|
1227
|
+
} catch {
|
|
1228
|
+
console.warn(chalk4.yellow(`\u8B66\u544A: \u65E0\u6CD5\u89E3\u6790 ${filePath}`));
|
|
1229
|
+
return {};
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return {};
|
|
1233
|
+
};
|
|
1234
|
+
var writeJsonFile = (filePath, data) => {
|
|
1235
|
+
fs5.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
1236
|
+
};
|
|
1237
|
+
var setupOnboarding = () => {
|
|
1238
|
+
const configPath = getGlobalClaudeConfigPath();
|
|
1239
|
+
try {
|
|
1240
|
+
const config3 = readJsonFile(configPath);
|
|
1241
|
+
if (config3.hasCompletedOnboarding === true) {
|
|
1242
|
+
console.log(chalk4.gray(" \u2713 hasCompletedOnboarding \u5DF2\u8BBE\u7F6E"));
|
|
1243
|
+
return true;
|
|
1244
|
+
}
|
|
1245
|
+
config3.hasCompletedOnboarding = true;
|
|
1246
|
+
writeJsonFile(configPath, config3);
|
|
1247
|
+
console.log(chalk4.green(" \u2713 \u5DF2\u8BBE\u7F6E hasCompletedOnboarding: true"));
|
|
1248
|
+
return true;
|
|
1249
|
+
} catch (err) {
|
|
1250
|
+
console.error(chalk4.red(` \u2717 \u8BBE\u7F6E hasCompletedOnboarding \u5931\u8D25: ${err}`));
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
var setupEnvSettings = () => {
|
|
1255
|
+
const settingsPath = getGlobalClaudeSettingsPath();
|
|
1256
|
+
try {
|
|
1257
|
+
ensureGlobalClaudeDir();
|
|
1258
|
+
const settings = readJsonFile(settingsPath);
|
|
1259
|
+
if (!settings.env || typeof settings.env !== "object") {
|
|
1260
|
+
settings.env = {};
|
|
1261
|
+
}
|
|
1262
|
+
const env = settings.env;
|
|
1263
|
+
const envVars = {
|
|
1264
|
+
"DISABLE_BUG_COMMAND": "1",
|
|
1265
|
+
"DISABLE_ERROR_REPORTING": "1",
|
|
1266
|
+
"DISABLE_TELEMETRY": "1"
|
|
1267
|
+
};
|
|
1268
|
+
let changed = false;
|
|
1269
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
1270
|
+
if (env[key] !== value) {
|
|
1271
|
+
env[key] = value;
|
|
1272
|
+
changed = true;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (!changed) {
|
|
1276
|
+
console.log(chalk4.gray(" \u2713 \u73AF\u5883\u53D8\u91CF\u5DF2\u914D\u7F6E"));
|
|
1277
|
+
return true;
|
|
1278
|
+
}
|
|
1279
|
+
writeJsonFile(settingsPath, settings);
|
|
1280
|
+
console.log(chalk4.green(" \u2713 \u5DF2\u914D\u7F6E\u73AF\u5883\u53D8\u91CF:"));
|
|
1281
|
+
console.log(chalk4.gray(" DISABLE_BUG_COMMAND=1"));
|
|
1282
|
+
console.log(chalk4.gray(" DISABLE_ERROR_REPORTING=1"));
|
|
1283
|
+
console.log(chalk4.gray(" DISABLE_TELEMETRY=1"));
|
|
1284
|
+
return true;
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
console.error(chalk4.red(` \u2717 \u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF\u5931\u8D25: ${err}`));
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
var setupMcpTool = () => {
|
|
1291
|
+
return new Promise((resolve2) => {
|
|
1292
|
+
console.log(chalk4.cyan(" \u2192 \u6B63\u5728\u6DFB\u52A0 chrome-devtools MCP \u5DE5\u5177..."));
|
|
1293
|
+
const child = spawn2("claude", [
|
|
1294
|
+
"mcp",
|
|
1295
|
+
"add",
|
|
1296
|
+
"chrome-devtools",
|
|
1297
|
+
"npx",
|
|
1298
|
+
"chrome-devtools-mcp@latest",
|
|
1299
|
+
"--scope",
|
|
1300
|
+
"user"
|
|
1301
|
+
], {
|
|
1302
|
+
stdio: "pipe",
|
|
1303
|
+
shell: true
|
|
1304
|
+
});
|
|
1305
|
+
let stdout = "";
|
|
1306
|
+
let stderr = "";
|
|
1307
|
+
child.stdout?.on("data", (data) => {
|
|
1308
|
+
stdout += data.toString();
|
|
1309
|
+
});
|
|
1310
|
+
child.stderr?.on("data", (data) => {
|
|
1311
|
+
stderr += data.toString();
|
|
1312
|
+
});
|
|
1313
|
+
child.on("exit", (code) => {
|
|
1314
|
+
if (code === 0) {
|
|
1315
|
+
console.log(chalk4.green(" \u2713 \u5DF2\u6DFB\u52A0 chrome-devtools MCP \u5DE5\u5177"));
|
|
1316
|
+
resolve2(true);
|
|
1317
|
+
} else {
|
|
1318
|
+
if (stderr.includes("already exists") || stdout.includes("already exists")) {
|
|
1319
|
+
console.log(chalk4.gray(" \u2713 chrome-devtools MCP \u5DE5\u5177\u5DF2\u5B58\u5728"));
|
|
1320
|
+
resolve2(true);
|
|
1321
|
+
} else {
|
|
1322
|
+
console.error(chalk4.red(` \u2717 \u6DFB\u52A0 MCP \u5DE5\u5177\u5931\u8D25 (code: ${code})`));
|
|
1323
|
+
if (stderr) console.error(chalk4.gray(` ${stderr.trim()}`));
|
|
1324
|
+
resolve2(false);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
child.on("error", (err) => {
|
|
1329
|
+
console.error(chalk4.red(` \u2717 \u6267\u884C claude \u547D\u4EE4\u5931\u8D25: ${err.message}`));
|
|
1330
|
+
console.log(chalk4.yellow(" \u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5 Claude Code CLI"));
|
|
1331
|
+
resolve2(false);
|
|
1332
|
+
});
|
|
1333
|
+
});
|
|
1334
|
+
};
|
|
1335
|
+
var runSetupInit = async () => {
|
|
1336
|
+
console.log(chalk4.bold("\n\u{1F527} Claude Code \u521D\u59CB\u5316\u8BBE\u7F6E\n"));
|
|
1337
|
+
console.log(chalk4.cyan("1. \u8BBE\u7F6E onboarding \u72B6\u6001"));
|
|
1338
|
+
const step1 = setupOnboarding();
|
|
1339
|
+
console.log(chalk4.cyan("\n2. \u914D\u7F6E\u9690\u79C1\u8BBE\u7F6E"));
|
|
1340
|
+
const step2 = setupEnvSettings();
|
|
1341
|
+
console.log(chalk4.cyan("\n3. \u5B89\u88C5 MCP \u5DE5\u5177"));
|
|
1342
|
+
const step3 = await setupMcpTool();
|
|
1343
|
+
console.log("");
|
|
1344
|
+
if (step1 && step2 && step3) {
|
|
1345
|
+
console.log(chalk4.green.bold("\u2705 \u521D\u59CB\u5316\u5B8C\u6210\uFF01"));
|
|
1346
|
+
} else {
|
|
1347
|
+
console.log(chalk4.yellow.bold("\u26A0\uFE0F \u90E8\u5206\u6B65\u9AA4\u672A\u5B8C\u6210\uFF0C\u8BF7\u68C0\u67E5\u4E0A\u8FF0\u9519\u8BEF"));
|
|
1348
|
+
}
|
|
1349
|
+
console.log(chalk4.gray("\n\u914D\u7F6E\u6587\u4EF6\u4F4D\u7F6E:"));
|
|
1350
|
+
console.log(chalk4.gray(` - ${getGlobalClaudeConfigPath()}`));
|
|
1351
|
+
console.log(chalk4.gray(` - ${getGlobalClaudeSettingsPath()}`));
|
|
1352
|
+
console.log("");
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
// src/skills.ts
|
|
1356
|
+
import { execSync } from "child_process";
|
|
1357
|
+
import * as fs6 from "fs";
|
|
1358
|
+
import * as path4 from "path";
|
|
1359
|
+
import chalk5 from "chalk";
|
|
1360
|
+
var SKILL_GROUPS = {
|
|
1361
|
+
official: { label: "\u5B98\u65B9", icon: "\u{1F3E2}" },
|
|
1362
|
+
featured: { label: "\u7CBE\u9009", icon: "\u2B50" },
|
|
1363
|
+
others: { label: "\u5176\u4ED6", icon: "\u{1F4E6}" }
|
|
1364
|
+
};
|
|
1365
|
+
var SKILL_PRESETS = [
|
|
1366
|
+
// ===== 官方 (Official) =====
|
|
1367
|
+
{
|
|
1368
|
+
name: "frontend-design",
|
|
1369
|
+
description: "\u521B\u5EFA\u9AD8\u8D28\u91CF\u524D\u7AEF\u754C\u9762\u8BBE\u8BA1",
|
|
1370
|
+
group: "official",
|
|
1371
|
+
install: { type: "preset", name: "frontend-design" }
|
|
1372
|
+
},
|
|
1373
|
+
{
|
|
1374
|
+
name: "skill-creator",
|
|
1375
|
+
description: "\u521B\u5EFA\u65B0\u7684 Claude Code skills",
|
|
1376
|
+
group: "official",
|
|
1377
|
+
install: { type: "preset", name: "skill-creator" }
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
name: "web-artifacts-builder",
|
|
1381
|
+
description: "\u6784\u5EFA\u53EF\u4EA4\u4E92\u7684 Web \u7EC4\u4EF6",
|
|
1382
|
+
group: "official",
|
|
1383
|
+
install: { type: "preset", name: "web-artifacts-builder" }
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
name: "canvas-design",
|
|
1387
|
+
description: "Canvas \u7ED8\u56FE\u8BBE\u8BA1",
|
|
1388
|
+
group: "official",
|
|
1389
|
+
install: { type: "preset", name: "canvas-design" }
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
name: "algorithmic-art",
|
|
1393
|
+
description: "\u7B97\u6CD5\u827A\u672F\u751F\u6210",
|
|
1394
|
+
group: "official",
|
|
1395
|
+
install: { type: "preset", name: "algorithmic-art" }
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
name: "theme-factory",
|
|
1399
|
+
description: "\u4E3B\u9898\u5DE5\u5382 - \u521B\u5EFA UI \u4E3B\u9898",
|
|
1400
|
+
group: "official",
|
|
1401
|
+
install: { type: "preset", name: "theme-factory" }
|
|
1402
|
+
},
|
|
1403
|
+
{
|
|
1404
|
+
name: "mcp-builder",
|
|
1405
|
+
description: "\u6784\u5EFA MCP \u670D\u52A1\u5668",
|
|
1406
|
+
group: "official",
|
|
1407
|
+
install: { type: "preset", name: "mcp-builder" }
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
name: "webapp-testing",
|
|
1411
|
+
description: "Web \u5E94\u7528\u6D4B\u8BD5",
|
|
1412
|
+
group: "official",
|
|
1413
|
+
install: { type: "preset", name: "webapp-testing" }
|
|
1414
|
+
},
|
|
1415
|
+
{
|
|
1416
|
+
name: "pdf",
|
|
1417
|
+
description: "PDF \u6587\u6863\u5904\u7406",
|
|
1418
|
+
group: "official",
|
|
1419
|
+
install: { type: "preset", name: "pdf" }
|
|
1420
|
+
},
|
|
1421
|
+
{
|
|
1422
|
+
name: "docx",
|
|
1423
|
+
description: "Word \u6587\u6863\u5904\u7406",
|
|
1424
|
+
group: "official",
|
|
1425
|
+
install: { type: "preset", name: "docx" }
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
name: "pptx",
|
|
1429
|
+
description: "PowerPoint \u6F14\u793A\u6587\u7A3F\u5904\u7406",
|
|
1430
|
+
group: "official",
|
|
1431
|
+
install: { type: "preset", name: "pptx" }
|
|
1432
|
+
},
|
|
1433
|
+
{
|
|
1434
|
+
name: "xlsx",
|
|
1435
|
+
description: "Excel \u8868\u683C\u5904\u7406",
|
|
1436
|
+
group: "official",
|
|
1437
|
+
install: { type: "preset", name: "xlsx" }
|
|
1438
|
+
},
|
|
1439
|
+
{
|
|
1440
|
+
name: "brand-guidelines",
|
|
1441
|
+
description: "\u54C1\u724C\u6307\u5357\u751F\u6210",
|
|
1442
|
+
group: "official",
|
|
1443
|
+
install: { type: "preset", name: "brand-guidelines" }
|
|
1444
|
+
},
|
|
1445
|
+
{
|
|
1446
|
+
name: "doc-coauthoring",
|
|
1447
|
+
description: "\u6587\u6863\u534F\u4F5C\u7F16\u5199",
|
|
1448
|
+
group: "official",
|
|
1449
|
+
install: { type: "preset", name: "doc-coauthoring" }
|
|
1450
|
+
},
|
|
1451
|
+
{
|
|
1452
|
+
name: "internal-comms",
|
|
1453
|
+
description: "\u5185\u90E8\u901A\u4FE1\u6587\u6863",
|
|
1454
|
+
group: "official",
|
|
1455
|
+
install: { type: "preset", name: "internal-comms" }
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
name: "slack-gif-creator",
|
|
1459
|
+
description: "Slack GIF \u521B\u5EFA\u5668",
|
|
1460
|
+
group: "official",
|
|
1461
|
+
install: { type: "preset", name: "slack-gif-creator" }
|
|
1462
|
+
},
|
|
1463
|
+
// ===== 精选 (Featured) =====
|
|
1464
|
+
{
|
|
1465
|
+
name: "superpowers",
|
|
1466
|
+
description: "Claude Code Plan\u6A21\u5F0F\u5347\u7EA7\u7248\uFF0C\u8FDE\u7EED\u8FFD\u95EE\u8BA8\u8BBA\u786E\u5B9A\u5F00\u53D1\u65B9\u6848",
|
|
1467
|
+
group: "featured",
|
|
1468
|
+
install: {
|
|
1469
|
+
type: "plugin",
|
|
1470
|
+
marketplace: "obra/superpowers-marketplace",
|
|
1471
|
+
package: "superpowers@superpowers-marketplace"
|
|
1472
|
+
}
|
|
1473
|
+
},
|
|
1474
|
+
{
|
|
1475
|
+
name: "ui-ux-pro-max",
|
|
1476
|
+
description: "\u4E13\u4E1A UI/UX \u8BBE\u8BA1",
|
|
1477
|
+
group: "featured",
|
|
1478
|
+
install: {
|
|
1479
|
+
type: "github",
|
|
1480
|
+
url: "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill/tree/main/.claude/skills/ui-ux-pro-max"
|
|
1481
|
+
}
|
|
1482
|
+
},
|
|
1483
|
+
{
|
|
1484
|
+
name: "Humanizer-zh",
|
|
1485
|
+
description: "\u53BB\u9664\u6587\u672C\u4E2D AI \u751F\u6210\u75D5\u8FF9\uFF0C\u6539\u5199\u5F97\u66F4\u81EA\u7136\u3001\u66F4\u50CF\u4EBA\u7C7B\u4E66\u5199",
|
|
1486
|
+
group: "featured",
|
|
1487
|
+
install: {
|
|
1488
|
+
type: "github",
|
|
1489
|
+
url: "https://github.com/op7418/Humanizer-zh"
|
|
1490
|
+
}
|
|
1491
|
+
},
|
|
1492
|
+
// ===== 其他 (Others) =====
|
|
1493
|
+
{
|
|
1494
|
+
name: "skill-writer",
|
|
1495
|
+
description: "\u6307\u5BFC\u7528\u6237\u4E3A Claude Code \u521B\u5EFA\u4EE3\u7406\u6280\u80FD",
|
|
1496
|
+
group: "others",
|
|
1497
|
+
install: {
|
|
1498
|
+
type: "github",
|
|
1499
|
+
url: "https://github.com/pytorch/pytorch/tree/main/.claude/skills/skill-writer"
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
];
|
|
1503
|
+
function getSkillsByGroup(group) {
|
|
1504
|
+
return SKILL_PRESETS.filter((p) => p.group === group);
|
|
1505
|
+
}
|
|
1506
|
+
function getGroupOrder() {
|
|
1507
|
+
return ["official", "featured", "others"];
|
|
1508
|
+
}
|
|
1509
|
+
function parseGitHubUrl(url) {
|
|
1510
|
+
if (/^[\w-]+\/[\w-]+$/.test(url)) {
|
|
1511
|
+
const [owner2, repo2] = url.split("/");
|
|
1512
|
+
return { owner: owner2, repo: repo2, branch: "main", path: "" };
|
|
1513
|
+
}
|
|
1514
|
+
const match = url.match(
|
|
1515
|
+
/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+)(?:\/(.*))?)?/
|
|
1516
|
+
);
|
|
1517
|
+
if (!match) return null;
|
|
1518
|
+
const [, owner, repo, branch = "main", repoPath = ""] = match;
|
|
1519
|
+
return {
|
|
1520
|
+
owner,
|
|
1521
|
+
repo: repo.replace(/\.git$/, ""),
|
|
1522
|
+
branch,
|
|
1523
|
+
path: repoPath
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
function getSkillsDir() {
|
|
1527
|
+
return path4.join(process.cwd(), ".claude", "skills");
|
|
1528
|
+
}
|
|
1529
|
+
function ensureSkillsDir() {
|
|
1530
|
+
const skillsDir = getSkillsDir();
|
|
1531
|
+
if (!fs6.existsSync(skillsDir)) {
|
|
1532
|
+
fs6.mkdirSync(skillsDir, { recursive: true });
|
|
1533
|
+
} else {
|
|
1534
|
+
cleanupTempDirs(skillsDir);
|
|
1535
|
+
}
|
|
1536
|
+
return skillsDir;
|
|
1537
|
+
}
|
|
1538
|
+
function cleanupTempDirs(skillsDir) {
|
|
1539
|
+
try {
|
|
1540
|
+
const entries = fs6.readdirSync(skillsDir, { withFileTypes: true });
|
|
1541
|
+
for (const entry of entries) {
|
|
1542
|
+
if (entry.isDirectory() && entry.name.startsWith(".tmp-")) {
|
|
1543
|
+
const tmpPath = path4.join(skillsDir, entry.name);
|
|
1544
|
+
fs6.rmSync(tmpPath, { recursive: true });
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
} catch {
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
function downloadSkillWithGit(owner, repo, branch, repoPath, targetName) {
|
|
1551
|
+
const skillsDir = ensureSkillsDir();
|
|
1552
|
+
const targetDir = path4.join(skillsDir, targetName);
|
|
1553
|
+
if (fs6.existsSync(targetDir)) {
|
|
1554
|
+
console.log(chalk5.yellow(`Skill "${targetName}" already exists. Updating...`));
|
|
1555
|
+
fs6.rmSync(targetDir, { recursive: true });
|
|
1556
|
+
}
|
|
1557
|
+
const repoUrl = `https://github.com/${owner}/${repo}.git`;
|
|
1558
|
+
const tempDir = path4.join(skillsDir, `.tmp-${Date.now()}`);
|
|
1559
|
+
try {
|
|
1560
|
+
fs6.mkdirSync(tempDir, { recursive: true });
|
|
1561
|
+
execSync(`git init`, { cwd: tempDir, stdio: "pipe" });
|
|
1562
|
+
execSync(`git remote add origin ${repoUrl}`, { cwd: tempDir, stdio: "pipe" });
|
|
1563
|
+
execSync(`git config core.sparseCheckout true`, { cwd: tempDir, stdio: "pipe" });
|
|
1564
|
+
const sparseFile = path4.join(tempDir, ".git", "info", "sparse-checkout");
|
|
1565
|
+
fs6.writeFileSync(sparseFile, repoPath ? `${repoPath}/
|
|
1566
|
+
` : "*\n");
|
|
1567
|
+
execSync(`git pull --depth=1 origin ${branch}`, { cwd: tempDir, stdio: "pipe" });
|
|
1568
|
+
const sourceDir = repoPath ? path4.join(tempDir, repoPath) : tempDir;
|
|
1569
|
+
if (!fs6.existsSync(sourceDir)) {
|
|
1570
|
+
throw new Error(`Path "${repoPath}" not found in repository`);
|
|
1571
|
+
}
|
|
1572
|
+
copyDir(sourceDir, targetDir);
|
|
1573
|
+
console.log(chalk5.green(`Successfully installed skill "${targetName}"`));
|
|
1574
|
+
return true;
|
|
1575
|
+
} catch (error) {
|
|
1576
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1577
|
+
console.error(chalk5.red(`Failed to download skill: ${errMsg}`));
|
|
1578
|
+
return false;
|
|
1579
|
+
} finally {
|
|
1580
|
+
if (fs6.existsSync(tempDir)) {
|
|
1581
|
+
fs6.rmSync(tempDir, { recursive: true });
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
function copyDir(src, dest) {
|
|
1586
|
+
fs6.mkdirSync(dest, { recursive: true });
|
|
1587
|
+
const entries = fs6.readdirSync(src, { withFileTypes: true });
|
|
1588
|
+
for (const entry of entries) {
|
|
1589
|
+
if (entry.name === ".git") continue;
|
|
1590
|
+
const srcPath = path4.join(src, entry.name);
|
|
1591
|
+
const destPath = path4.join(dest, entry.name);
|
|
1592
|
+
if (entry.isDirectory()) {
|
|
1593
|
+
copyDir(srcPath, destPath);
|
|
1594
|
+
} else {
|
|
1595
|
+
fs6.copyFileSync(srcPath, destPath);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
function addSkillFromGitHub(urlOrPreset) {
|
|
1600
|
+
const preset = SKILL_PRESETS.find((p) => p.name === urlOrPreset);
|
|
1601
|
+
if (preset) {
|
|
1602
|
+
if (preset.install.type === "preset") {
|
|
1603
|
+
return downloadSkillWithGit(
|
|
1604
|
+
"anthropics",
|
|
1605
|
+
"skills",
|
|
1606
|
+
"main",
|
|
1607
|
+
`skills/${preset.install.name}`,
|
|
1608
|
+
preset.name
|
|
1609
|
+
);
|
|
1610
|
+
} else if (preset.install.type === "github") {
|
|
1611
|
+
const parsed2 = parseGitHubUrl(preset.install.url);
|
|
1612
|
+
if (!parsed2) {
|
|
1613
|
+
console.error(chalk5.red(`Invalid GitHub URL in preset: ${preset.install.url}`));
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1616
|
+
return downloadSkillWithGit(
|
|
1617
|
+
parsed2.owner,
|
|
1618
|
+
parsed2.repo,
|
|
1619
|
+
parsed2.branch,
|
|
1620
|
+
parsed2.path,
|
|
1621
|
+
preset.name
|
|
1622
|
+
);
|
|
1623
|
+
} else if (preset.install.type === "plugin") {
|
|
1624
|
+
console.error(chalk5.yellow(`Plugin installation not yet supported for "${preset.name}"`));
|
|
1625
|
+
console.log(chalk5.gray(`Marketplace: ${preset.install.marketplace}`));
|
|
1626
|
+
console.log(chalk5.gray(`Package: ${preset.install.package}`));
|
|
1627
|
+
return false;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
const parsed = parseGitHubUrl(urlOrPreset);
|
|
1631
|
+
if (!parsed) {
|
|
1632
|
+
console.error(chalk5.red("Invalid GitHub URL or preset name"));
|
|
1633
|
+
console.log(chalk5.gray("Examples:"));
|
|
1634
|
+
console.log(chalk5.gray(" ccem skill add frontend-design"));
|
|
1635
|
+
console.log(chalk5.gray(" ccem skill add https://github.com/owner/repo"));
|
|
1636
|
+
console.log(chalk5.gray(" ccem skill add https://github.com/owner/repo/tree/main/path"));
|
|
1637
|
+
return false;
|
|
1638
|
+
}
|
|
1639
|
+
let skillName;
|
|
1640
|
+
if (parsed.path) {
|
|
1641
|
+
skillName = path4.basename(parsed.path);
|
|
1642
|
+
} else {
|
|
1643
|
+
skillName = parsed.repo;
|
|
1644
|
+
}
|
|
1645
|
+
return downloadSkillWithGit(
|
|
1646
|
+
parsed.owner,
|
|
1647
|
+
parsed.repo,
|
|
1648
|
+
parsed.branch,
|
|
1649
|
+
parsed.path,
|
|
1650
|
+
skillName
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
function listInstalledSkills() {
|
|
1654
|
+
const skillsDir = getSkillsDir();
|
|
1655
|
+
if (!fs6.existsSync(skillsDir)) {
|
|
1656
|
+
return [];
|
|
1657
|
+
}
|
|
1658
|
+
const entries = fs6.readdirSync(skillsDir, { withFileTypes: true });
|
|
1659
|
+
return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => ({
|
|
1660
|
+
name: entry.name,
|
|
1661
|
+
path: path4.join(skillsDir, entry.name)
|
|
1662
|
+
}));
|
|
1663
|
+
}
|
|
1664
|
+
function removeSkill(name) {
|
|
1665
|
+
const skillsDir = getSkillsDir();
|
|
1666
|
+
const targetDir = path4.join(skillsDir, name);
|
|
1667
|
+
if (!fs6.existsSync(targetDir)) {
|
|
1668
|
+
console.error(chalk5.red(`Skill "${name}" not found`));
|
|
1669
|
+
return false;
|
|
1670
|
+
}
|
|
1671
|
+
fs6.rmSync(targetDir, { recursive: true });
|
|
1672
|
+
console.log(chalk5.green(`Removed skill "${name}"`));
|
|
1673
|
+
return true;
|
|
1674
|
+
}
|
|
1675
|
+
function installFromPluginMarketplace(marketplace, packageName) {
|
|
1676
|
+
try {
|
|
1677
|
+
console.log(chalk5.cyan(`Adding marketplace: ${marketplace}...`));
|
|
1678
|
+
execSync(`claude plugin marketplace add ${marketplace}`, { stdio: "inherit" });
|
|
1679
|
+
console.log(chalk5.cyan(`Installing package: ${packageName}...`));
|
|
1680
|
+
execSync(`claude plugin install ${packageName}`, { stdio: "inherit" });
|
|
1681
|
+
console.log(chalk5.green(`Successfully installed ${packageName}`));
|
|
1682
|
+
return true;
|
|
1683
|
+
} catch (error) {
|
|
1684
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1685
|
+
console.error(chalk5.red(`Failed to install from marketplace: ${errMsg}`));
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
function installSkill(preset) {
|
|
1690
|
+
console.log(chalk5.cyan(`Installing ${preset.name}...`));
|
|
1691
|
+
switch (preset.install.type) {
|
|
1692
|
+
case "preset":
|
|
1693
|
+
const officialPreset = {
|
|
1694
|
+
repo: "anthropics/skills",
|
|
1695
|
+
path: `skills/${preset.install.name}`,
|
|
1696
|
+
branch: "main"
|
|
1697
|
+
};
|
|
1698
|
+
const [owner, repo] = officialPreset.repo.split("/");
|
|
1699
|
+
return downloadSkillWithGit(
|
|
1700
|
+
owner,
|
|
1701
|
+
repo,
|
|
1702
|
+
officialPreset.branch,
|
|
1703
|
+
officialPreset.path,
|
|
1704
|
+
preset.name
|
|
1705
|
+
);
|
|
1706
|
+
case "github":
|
|
1707
|
+
return addSkillFromGitHub(preset.install.url);
|
|
1708
|
+
case "plugin":
|
|
1709
|
+
return installFromPluginMarketplace(
|
|
1710
|
+
preset.install.marketplace,
|
|
1711
|
+
preset.install.package
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// src/components/index.tsx
|
|
1717
|
+
import React2 from "react";
|
|
1718
|
+
import { render } from "ink";
|
|
1719
|
+
|
|
1720
|
+
// src/components/SkillSelector.tsx
|
|
1721
|
+
import React, { useState, useEffect } from "react";
|
|
1722
|
+
import { Box, Text, useInput, useApp } from "ink";
|
|
1723
|
+
function SkillSelector({ onSelect, onCancel }) {
|
|
1724
|
+
const { exit } = useApp();
|
|
1725
|
+
const groups = getGroupOrder();
|
|
1726
|
+
const [activeGroupIndex, setActiveGroupIndex] = useState(0);
|
|
1727
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
1728
|
+
const currentGroup = groups[activeGroupIndex];
|
|
1729
|
+
const skills = getSkillsByGroup(currentGroup);
|
|
1730
|
+
const items = [...skills, null];
|
|
1731
|
+
const maxIndex = items.length - 1;
|
|
1732
|
+
useEffect(() => {
|
|
1733
|
+
setSelectedIndex(0);
|
|
1734
|
+
}, [activeGroupIndex]);
|
|
1735
|
+
useInput((input, key) => {
|
|
1736
|
+
if (key.tab && !key.shift) {
|
|
1737
|
+
setActiveGroupIndex((prev) => (prev + 1) % groups.length);
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
if (key.tab && key.shift) {
|
|
1741
|
+
setActiveGroupIndex((prev) => (prev - 1 + groups.length) % groups.length);
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
if (key.upArrow) {
|
|
1745
|
+
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
if (key.downArrow) {
|
|
1749
|
+
setSelectedIndex((prev) => Math.min(maxIndex, prev + 1));
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
if (key.return) {
|
|
1753
|
+
const selected = items[selectedIndex];
|
|
1754
|
+
if (selected === null) {
|
|
1755
|
+
onSelect("custom");
|
|
1756
|
+
} else {
|
|
1757
|
+
onSelect(selected);
|
|
1758
|
+
}
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
if (key.escape || input === "q") {
|
|
1762
|
+
onCancel();
|
|
1763
|
+
exit();
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
});
|
|
1767
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, groups.map((group, index) => {
|
|
1768
|
+
const meta = SKILL_GROUPS[group];
|
|
1769
|
+
const isActive = index === activeGroupIndex;
|
|
1770
|
+
return /* @__PURE__ */ React.createElement(Box, { key: group, marginRight: 2 }, /* @__PURE__ */ React.createElement(
|
|
1771
|
+
Text,
|
|
1772
|
+
{
|
|
1773
|
+
bold: isActive,
|
|
1774
|
+
color: isActive ? "cyan" : "gray",
|
|
1775
|
+
inverse: isActive
|
|
1776
|
+
},
|
|
1777
|
+
" ",
|
|
1778
|
+
meta.icon,
|
|
1779
|
+
" ",
|
|
1780
|
+
meta.label,
|
|
1781
|
+
" "
|
|
1782
|
+
));
|
|
1783
|
+
})), /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "\u2500".repeat(50))), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, items.map((item, index) => {
|
|
1784
|
+
const isSelected = index === selectedIndex;
|
|
1785
|
+
const prefix = isSelected ? "\u276F " : " ";
|
|
1786
|
+
if (item === null) {
|
|
1787
|
+
return /* @__PURE__ */ React.createElement(Box, { key: "custom" }, /* @__PURE__ */ React.createElement(Text, { color: isSelected ? "yellow" : "gray" }, prefix, "\u8F93\u5165\u81EA\u5B9A\u4E49 GitHub URL"));
|
|
1788
|
+
}
|
|
1789
|
+
return /* @__PURE__ */ React.createElement(Box, { key: item.name }, /* @__PURE__ */ React.createElement(Text, { color: isSelected ? "cyan" : void 0 }, prefix, /* @__PURE__ */ React.createElement(Text, { bold: isSelected }, item.name), /* @__PURE__ */ React.createElement(Text, { color: "gray" }, " - ", item.description)));
|
|
1790
|
+
})), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "Tab \u5207\u6362\u5206\u7EC4 | \u2191\u2193 \u9009\u62E9 | Enter \u786E\u8BA4 | Esc \u53D6\u6D88")));
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// src/components/index.tsx
|
|
1794
|
+
async function runSkillSelector() {
|
|
1795
|
+
return new Promise((resolve2) => {
|
|
1796
|
+
let resolved = false;
|
|
1797
|
+
const { unmount, waitUntilExit } = render(
|
|
1798
|
+
/* @__PURE__ */ React2.createElement(
|
|
1799
|
+
SkillSelector,
|
|
1800
|
+
{
|
|
1801
|
+
onSelect: (result) => {
|
|
1802
|
+
if (resolved) return;
|
|
1803
|
+
resolved = true;
|
|
1804
|
+
unmount();
|
|
1805
|
+
if (result === "custom") {
|
|
1806
|
+
resolve2({ type: "custom" });
|
|
1807
|
+
} else {
|
|
1808
|
+
resolve2({ type: "skill", skill: result });
|
|
1809
|
+
}
|
|
1810
|
+
},
|
|
1811
|
+
onCancel: () => {
|
|
1812
|
+
if (resolved) return;
|
|
1813
|
+
resolved = true;
|
|
1814
|
+
unmount();
|
|
1815
|
+
resolve2({ type: "cancelled" });
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
)
|
|
1819
|
+
);
|
|
1820
|
+
waitUntilExit().then(() => {
|
|
1821
|
+
if (!resolved) {
|
|
1822
|
+
resolve2({ type: "cancelled" });
|
|
1823
|
+
}
|
|
1824
|
+
});
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// src/remote.ts
|
|
1829
|
+
import crypto2 from "crypto";
|
|
1830
|
+
import chalk6 from "chalk";
|
|
1831
|
+
import Conf from "conf";
|
|
1832
|
+
import { encrypt } from "@ccem/core";
|
|
1833
|
+
var config = new Conf({
|
|
1834
|
+
projectName: "claude-code-env-manager"
|
|
1835
|
+
});
|
|
1836
|
+
var decryptWithSecret = (encryptedBase64, secret) => {
|
|
1837
|
+
const key = crypto2.scryptSync(secret, "ccem-salt", 32);
|
|
1838
|
+
const combined = Buffer.from(encryptedBase64, "base64");
|
|
1839
|
+
const iv = combined.subarray(0, 16);
|
|
1840
|
+
const encryptedHex = combined.subarray(16).toString("hex");
|
|
1841
|
+
const decipher = crypto2.createDecipheriv("aes-256-cbc", key, iv);
|
|
1842
|
+
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
1843
|
+
decrypted += decipher.final("utf8");
|
|
1844
|
+
return decrypted;
|
|
1845
|
+
};
|
|
1846
|
+
var getUniqueName = (baseName, existingNames) => {
|
|
1847
|
+
if (!existingNames.has(baseName)) {
|
|
1848
|
+
return baseName;
|
|
1849
|
+
}
|
|
1850
|
+
let suffix = 1;
|
|
1851
|
+
let newName = `${baseName}-remote`;
|
|
1852
|
+
while (existingNames.has(newName)) {
|
|
1853
|
+
suffix++;
|
|
1854
|
+
newName = `${baseName}-remote-${suffix}`;
|
|
1855
|
+
}
|
|
1856
|
+
return newName;
|
|
1857
|
+
};
|
|
1858
|
+
var loadFromRemote = async (url, secret) => {
|
|
1859
|
+
console.log(chalk6.gray("Fetching from remote..."));
|
|
1860
|
+
let response;
|
|
1861
|
+
try {
|
|
1862
|
+
response = await fetch(url);
|
|
1863
|
+
} catch (err) {
|
|
1864
|
+
console.error(chalk6.red("Error: Failed to connect to server"));
|
|
1865
|
+
console.error(chalk6.gray(err.message));
|
|
1866
|
+
process.exit(1);
|
|
1867
|
+
}
|
|
1868
|
+
if (response.status === 401) {
|
|
1869
|
+
console.error(chalk6.red("Error: Invalid key (HTTP 401)"));
|
|
1870
|
+
process.exit(1);
|
|
1871
|
+
}
|
|
1872
|
+
if (response.status === 429) {
|
|
1873
|
+
console.error(chalk6.red("Error: Too many requests, please try again later"));
|
|
1874
|
+
process.exit(1);
|
|
1875
|
+
}
|
|
1876
|
+
if (!response.ok) {
|
|
1877
|
+
console.error(chalk6.red(`Error: Server returned HTTP ${response.status}`));
|
|
1878
|
+
process.exit(1);
|
|
1879
|
+
}
|
|
1880
|
+
let data;
|
|
1881
|
+
try {
|
|
1882
|
+
data = await response.json();
|
|
1883
|
+
} catch {
|
|
1884
|
+
console.error(chalk6.red("Error: Invalid response format from server"));
|
|
1885
|
+
process.exit(1);
|
|
1886
|
+
}
|
|
1887
|
+
if (!data.encrypted) {
|
|
1888
|
+
console.error(chalk6.red("Error: Invalid response format from server"));
|
|
1889
|
+
process.exit(1);
|
|
1890
|
+
}
|
|
1891
|
+
let decrypted;
|
|
1892
|
+
try {
|
|
1893
|
+
const jsonStr = decryptWithSecret(data.encrypted, secret);
|
|
1894
|
+
decrypted = JSON.parse(jsonStr);
|
|
1895
|
+
} catch {
|
|
1896
|
+
console.error(chalk6.red("Error: Decryption failed, check your --secret"));
|
|
1897
|
+
process.exit(1);
|
|
1898
|
+
}
|
|
1899
|
+
if (!decrypted.environments || typeof decrypted.environments !== "object") {
|
|
1900
|
+
console.error(chalk6.red("Error: Invalid response format from server"));
|
|
1901
|
+
process.exit(1);
|
|
1902
|
+
}
|
|
1903
|
+
const registries = config.get("registries");
|
|
1904
|
+
const existingNames = new Set(Object.keys(registries));
|
|
1905
|
+
const results = [];
|
|
1906
|
+
for (const [name, envConfig] of Object.entries(decrypted.environments)) {
|
|
1907
|
+
const uniqueName = getUniqueName(name, existingNames);
|
|
1908
|
+
const renamed = uniqueName !== name;
|
|
1909
|
+
const configToSave = { ...envConfig };
|
|
1910
|
+
if (configToSave.ANTHROPIC_API_KEY) {
|
|
1911
|
+
configToSave.ANTHROPIC_API_KEY = encrypt(configToSave.ANTHROPIC_API_KEY);
|
|
1912
|
+
}
|
|
1913
|
+
registries[uniqueName] = configToSave;
|
|
1914
|
+
existingNames.add(uniqueName);
|
|
1915
|
+
results.push({
|
|
1916
|
+
name: uniqueName,
|
|
1917
|
+
originalName: name,
|
|
1918
|
+
renamed
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
config.set("registries", registries);
|
|
1922
|
+
console.log(chalk6.green(`
|
|
1923
|
+
Loaded ${results.length} environment(s) from remote:`));
|
|
1924
|
+
for (const result of results) {
|
|
1925
|
+
if (result.renamed) {
|
|
1926
|
+
console.log(chalk6.yellow(` + ${result.originalName} \u2192 ${result.name} (renamed, local exists)`));
|
|
1927
|
+
} else {
|
|
1928
|
+
console.log(chalk6.green(` + ${result.name} (new)`));
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
console.log(chalk6.gray("\nRun 'ccem ls' to see all environments."));
|
|
1932
|
+
};
|
|
1933
|
+
|
|
1934
|
+
// src/cron-skill.ts
|
|
1935
|
+
var CCEM_CRON_SKILL_CONTENT = `# ccem-cron
|
|
1936
|
+
|
|
1937
|
+
Manage scheduled tasks for Claude Code via \\\`~/.ccem/cron-tasks.json\\\`. Supports creating, listing, and deleting cron tasks through conversational interaction.
|
|
1938
|
+
|
|
1939
|
+
## Storage
|
|
1940
|
+
|
|
1941
|
+
All tasks are stored in \\\`~/.ccem/cron-tasks.json\\\` as a JSON array. Each task follows this schema:
|
|
1942
|
+
|
|
1943
|
+
\\\`\\\`\\\`json
|
|
1944
|
+
{
|
|
1945
|
+
"id": "uuid-v4",
|
|
1946
|
+
"name": "task name",
|
|
1947
|
+
"cronExpression": "0 9 * * *",
|
|
1948
|
+
"prompt": "Claude prompt to execute",
|
|
1949
|
+
"workingDir": "/absolute/path",
|
|
1950
|
+
"envName": null,
|
|
1951
|
+
"enabled": true,
|
|
1952
|
+
"timeoutSecs": 300,
|
|
1953
|
+
"templateId": null,
|
|
1954
|
+
"triggerType": "schedule",
|
|
1955
|
+
"parentTaskId": null,
|
|
1956
|
+
"createdAt": "ISO-8601",
|
|
1957
|
+
"updatedAt": "ISO-8601"
|
|
1958
|
+
}
|
|
1959
|
+
\\\`\\\`\\\`
|
|
1960
|
+
|
|
1961
|
+
## Instructions
|
|
1962
|
+
|
|
1963
|
+
Determine the user's intent from their message:
|
|
1964
|
+
- **List/view**: user says "list", "show", "view", "\u67E5\u770B", "\u5217\u51FA"
|
|
1965
|
+
- **Delete/remove**: user says "delete", "remove", "\u5220\u9664", "\u79FB\u9664"
|
|
1966
|
+
- **Create**: default for anything else
|
|
1967
|
+
|
|
1968
|
+
### Creating a Task
|
|
1969
|
+
|
|
1970
|
+
1. Ask the user for:
|
|
1971
|
+
- Task name (short descriptive label)
|
|
1972
|
+
- What they want Claude to do (natural language; derive the \\\`prompt\\\` field from this)
|
|
1973
|
+
- When to run it (derive the \\\`cronExpression\\\`; show common examples below)
|
|
1974
|
+
- Working directory (default: current directory via \\\`pwd\\\`)
|
|
1975
|
+
- Timeout in seconds (default: 300)
|
|
1976
|
+
|
|
1977
|
+
2. Show common cron patterns to help the user choose:
|
|
1978
|
+
|
|
1979
|
+
\\\`\\\`\\\`
|
|
1980
|
+
Every minute: * * * * *
|
|
1981
|
+
Every 30 minutes: */30 * * * *
|
|
1982
|
+
Every hour: 0 * * * *
|
|
1983
|
+
Every day at 9am: 0 9 * * *
|
|
1984
|
+
Every day at midnight: 0 0 * * *
|
|
1985
|
+
Weekdays at 9am: 0 9 * * 1-5
|
|
1986
|
+
Every Monday 8am: 0 8 * * 1
|
|
1987
|
+
Every 1st of month: 0 0 1 * *
|
|
1988
|
+
\\\`\\\`\\\`
|
|
1989
|
+
|
|
1990
|
+
3. Generate the task and write it:
|
|
1991
|
+
|
|
1992
|
+
\\\`\\\`\\\`bash
|
|
1993
|
+
# Generate UUID and timestamp
|
|
1994
|
+
TASK_ID=\\$(uuidgen | tr '[:upper:]' '[:lower:]')
|
|
1995
|
+
NOW=\\$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
|
1996
|
+
WORK_DIR=\\$(pwd)
|
|
1997
|
+
|
|
1998
|
+
# Read existing file or initialize empty array
|
|
1999
|
+
if [ -f ~/.ccem/cron-tasks.json ]; then
|
|
2000
|
+
EXISTING=\\$(cat ~/.ccem/cron-tasks.json)
|
|
2001
|
+
else
|
|
2002
|
+
mkdir -p ~/.ccem
|
|
2003
|
+
EXISTING="[]"
|
|
2004
|
+
fi
|
|
2005
|
+
|
|
2006
|
+
# Build new task JSON and append using python3 for safe JSON manipulation
|
|
2007
|
+
python3 -c "
|
|
2008
|
+
import json, sys
|
|
2009
|
+
tasks = json.loads('''\\$EXISTING''')
|
|
2010
|
+
tasks.append({
|
|
2011
|
+
'id': '\\$TASK_ID',
|
|
2012
|
+
'name': 'TASK_NAME_HERE',
|
|
2013
|
+
'cronExpression': 'CRON_EXPR_HERE',
|
|
2014
|
+
'prompt': 'PROMPT_HERE',
|
|
2015
|
+
'workingDir': '\\$WORK_DIR',
|
|
2016
|
+
'envName': None,
|
|
2017
|
+
'enabled': True,
|
|
2018
|
+
'timeoutSecs': TIMEOUT_HERE,
|
|
2019
|
+
'templateId': None,
|
|
2020
|
+
'triggerType': 'schedule',
|
|
2021
|
+
'parentTaskId': None,
|
|
2022
|
+
'createdAt': '\\$NOW',
|
|
2023
|
+
'updatedAt': '\\$NOW'
|
|
2024
|
+
})
|
|
2025
|
+
print(json.dumps(tasks, indent=2, ensure_ascii=False))
|
|
2026
|
+
" > ~/.ccem/cron-tasks.json
|
|
2027
|
+
\\\`\\\`\\\`
|
|
2028
|
+
|
|
2029
|
+
Replace \\\`TASK_NAME_HERE\\\`, \\\`CRON_EXPR_HERE\\\`, \\\`PROMPT_HERE\\\`, and \\\`TIMEOUT_HERE\\\` with actual values. Escape any quotes or special characters in the prompt string properly for Python.
|
|
2030
|
+
|
|
2031
|
+
4. Confirm creation by reading back the file and showing the new task.
|
|
2032
|
+
|
|
2033
|
+
### Listing Tasks
|
|
2034
|
+
|
|
2035
|
+
\\\`\\\`\\\`bash
|
|
2036
|
+
if [ -f ~/.ccem/cron-tasks.json ]; then
|
|
2037
|
+
cat ~/.ccem/cron-tasks.json
|
|
2038
|
+
else
|
|
2039
|
+
echo "No tasks found. File ~/.ccem/cron-tasks.json does not exist."
|
|
2040
|
+
fi
|
|
2041
|
+
\\\`\\\`\\\`
|
|
2042
|
+
|
|
2043
|
+
Format the output as a readable table with columns: name, cron expression, enabled status, working directory, and creation date. If the list is empty, tell the user.
|
|
2044
|
+
|
|
2045
|
+
### Deleting a Task
|
|
2046
|
+
|
|
2047
|
+
1. First list all tasks so the user can identify which to delete.
|
|
2048
|
+
2. Ask the user to confirm by name or ID.
|
|
2049
|
+
3. Remove the matching task:
|
|
2050
|
+
|
|
2051
|
+
\\\`\\\`\\\`bash
|
|
2052
|
+
python3 -c "
|
|
2053
|
+
import json
|
|
2054
|
+
with open('\\$HOME/.ccem/cron-tasks.json') as f:
|
|
2055
|
+
tasks = json.load(f)
|
|
2056
|
+
tasks = [t for t in tasks if t['id'] != 'TARGET_ID' and t['name'] != 'TARGET_NAME']
|
|
2057
|
+
with open('\\$HOME/.ccem/cron-tasks.json', 'w') as f:
|
|
2058
|
+
json.dump(tasks, f, indent=2, ensure_ascii=False)
|
|
2059
|
+
print(json.dumps(tasks, indent=2, ensure_ascii=False))
|
|
2060
|
+
"
|
|
2061
|
+
\\\`\\\`\\\`
|
|
2062
|
+
|
|
2063
|
+
Replace \\\`TARGET_ID\\\` or \\\`TARGET_NAME\\\` with the user's selection.
|
|
2064
|
+
|
|
2065
|
+
4. Confirm deletion by showing the remaining tasks.
|
|
2066
|
+
|
|
2067
|
+
## Safety Rules
|
|
2068
|
+
|
|
2069
|
+
- Always read the existing file before writing to avoid data loss.
|
|
2070
|
+
- Use \\\`python3\\\` for JSON manipulation to ensure valid output (never hand-construct JSON with echo/cat).
|
|
2071
|
+
- Create \\\`~/.ccem/\\\` directory if it does not exist.
|
|
2072
|
+
- When the file is missing or empty, start with an empty array \\\`[]\\\`.
|
|
2073
|
+
- Always show the user what will be written before confirming.
|
|
2074
|
+
`;
|
|
2075
|
+
|
|
2076
|
+
// src/index.ts
|
|
2077
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
2078
|
+
var __dirname2 = path5.dirname(__filename);
|
|
2079
|
+
var pkgPath = path5.resolve(__dirname2, "..", "package.json");
|
|
2080
|
+
var pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
2081
|
+
var program = new Command();
|
|
2082
|
+
ensureCcemDir3();
|
|
2083
|
+
var config2 = new Conf2({
|
|
2084
|
+
projectName: "claude-code-env-manager",
|
|
2085
|
+
cwd: getCcemConfigDir(),
|
|
2086
|
+
// 使用新路径
|
|
2087
|
+
defaults: {
|
|
2088
|
+
registries: {
|
|
2089
|
+
"official": {
|
|
2090
|
+
ANTHROPIC_BASE_URL: "https://api.anthropic.com",
|
|
2091
|
+
ANTHROPIC_MODEL: "claude-sonnet-4-5-20250929",
|
|
2092
|
+
ANTHROPIC_SMALL_FAST_MODEL: "claude-haiku-4-5-20251001"
|
|
2093
|
+
}
|
|
2094
|
+
},
|
|
2095
|
+
current: "official",
|
|
2096
|
+
defaultMode: null
|
|
2097
|
+
}
|
|
2098
|
+
});
|
|
2099
|
+
var PERMISSION_MODES = ["yolo", "dev", "readonly", "safe", "ci", "audit"];
|
|
2100
|
+
var usageStats = null;
|
|
2101
|
+
var usageLoading = true;
|
|
2102
|
+
var usageAbortController = null;
|
|
2103
|
+
var initUsageStats = (onUpdate) => {
|
|
2104
|
+
const cachedStats = getUsageStatsFromCache();
|
|
2105
|
+
if (cachedStats) {
|
|
2106
|
+
usageStats = cachedStats;
|
|
2107
|
+
usageLoading = false;
|
|
2108
|
+
} else {
|
|
2109
|
+
if (onUpdate) {
|
|
2110
|
+
startSpinner(onUpdate);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
if (usageAbortController) {
|
|
2114
|
+
usageAbortController.abort();
|
|
2115
|
+
}
|
|
2116
|
+
usageAbortController = new AbortController();
|
|
2117
|
+
const signal = usageAbortController.signal;
|
|
2118
|
+
getUsageStats(signal).then((stats) => {
|
|
2119
|
+
if (signal.aborted) return;
|
|
2120
|
+
const needRefresh = usageLoading || usageStats && stats && usageStats.lastUpdated !== stats.lastUpdated;
|
|
2121
|
+
usageStats = stats;
|
|
2122
|
+
usageLoading = false;
|
|
2123
|
+
stopSpinner();
|
|
2124
|
+
if (needRefresh && onUpdate) {
|
|
2125
|
+
onUpdate();
|
|
2126
|
+
}
|
|
2127
|
+
}).catch((err) => {
|
|
2128
|
+
if (err.message === "Aborted") return;
|
|
2129
|
+
usageLoading = false;
|
|
2130
|
+
stopSpinner();
|
|
2131
|
+
});
|
|
2132
|
+
};
|
|
2133
|
+
program.name("ccem").description("Claude Code Environment Manager - \u7BA1\u7406 Claude Code \u73AF\u5883\u53D8\u91CF\u548C\u6743\u9650").version(pkg.version).option("--mode", "\u67E5\u770B\u5F53\u524D\u6743\u9650\u6A21\u5F0F").option("--list-modes", "\u5217\u51FA\u6240\u6709\u53EF\u7528\u6743\u9650\u6A21\u5F0F");
|
|
2134
|
+
PERMISSION_MODES.forEach((mode) => {
|
|
2135
|
+
const preset = PERMISSION_PRESETS4[mode];
|
|
2136
|
+
program.command(mode).description(`\u4E34\u65F6\u5E94\u7528 ${preset.name}\uFF0C\u9000\u51FA\u540E\u8FD8\u539F`).action(async () => {
|
|
2137
|
+
const registries = config2.get("registries");
|
|
2138
|
+
const current = config2.get("current");
|
|
2139
|
+
const envConfig = registries[current];
|
|
2140
|
+
await runWithTempPermissions(mode, envConfig);
|
|
2141
|
+
});
|
|
2142
|
+
});
|
|
2143
|
+
var showCurrentEnv = (usageStats2, usageLoading2) => {
|
|
2144
|
+
if (!process.stdout.isTTY) return;
|
|
2145
|
+
const current = config2.get("current");
|
|
2146
|
+
const registries = config2.get("registries");
|
|
2147
|
+
const env = registries[current];
|
|
2148
|
+
const defaultMode = config2.get("defaultMode");
|
|
2149
|
+
if (!env) return;
|
|
2150
|
+
console.log(renderLogoWithEnvPanel(current, {
|
|
2151
|
+
ANTHROPIC_BASE_URL: env.ANTHROPIC_BASE_URL,
|
|
2152
|
+
ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY ? decrypt2(env.ANTHROPIC_API_KEY) : void 0,
|
|
2153
|
+
ANTHROPIC_MODEL: env.ANTHROPIC_MODEL,
|
|
2154
|
+
ANTHROPIC_SMALL_FAST_MODEL: env.ANTHROPIC_SMALL_FAST_MODEL
|
|
2155
|
+
}, defaultMode));
|
|
2156
|
+
console.log("");
|
|
2157
|
+
console.log(renderUsageLine(usageStats2, usageLoading2));
|
|
2158
|
+
console.log(renderCompactHeader());
|
|
2159
|
+
console.log("");
|
|
2160
|
+
};
|
|
2161
|
+
var switchEnvironment = async (name) => {
|
|
2162
|
+
const registries = config2.get("registries");
|
|
2163
|
+
if (!registries[name]) {
|
|
2164
|
+
console.error(chalk7.red(`Environment '${name}' not found.`));
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
config2.set("current", name);
|
|
2168
|
+
if (process.stdout.isTTY) {
|
|
2169
|
+
console.log(chalk7.green(`Switched to environment '${name}'`));
|
|
2170
|
+
} else {
|
|
2171
|
+
console.error(chalk7.green(`Switched to environment '${name}'`));
|
|
2172
|
+
}
|
|
2173
|
+
showCurrentEnv(null, false);
|
|
2174
|
+
const env = registries[name];
|
|
2175
|
+
const exportCmds = [];
|
|
2176
|
+
if (env.ANTHROPIC_BASE_URL) exportCmds.push(`export ANTHROPIC_BASE_URL="${env.ANTHROPIC_BASE_URL}"`);
|
|
2177
|
+
if (env.ANTHROPIC_API_KEY) exportCmds.push(`export ANTHROPIC_API_KEY="${decrypt2(env.ANTHROPIC_API_KEY)}"`);
|
|
2178
|
+
if (env.ANTHROPIC_MODEL) exportCmds.push(`export ANTHROPIC_MODEL="${env.ANTHROPIC_MODEL}"`);
|
|
2179
|
+
if (env.ANTHROPIC_SMALL_FAST_MODEL) exportCmds.push(`export ANTHROPIC_SMALL_FAST_MODEL="${env.ANTHROPIC_SMALL_FAST_MODEL}"`);
|
|
2180
|
+
if (process.stdout.isTTY) {
|
|
2181
|
+
console.log(chalk7.yellow("\nTo apply to current shell immediately, run:"));
|
|
2182
|
+
console.log(chalk7.cyan("eval $(ccem env)"));
|
|
2183
|
+
console.log(chalk7.yellow("\nOr manually export:"));
|
|
2184
|
+
exportCmds.forEach((cmd) => console.log(cmd));
|
|
2185
|
+
} else {
|
|
2186
|
+
exportCmds.forEach((cmd) => console.log(cmd));
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
program.command("ls").description("List all configured environments").action(() => {
|
|
2190
|
+
const registries = config2.get("registries");
|
|
2191
|
+
const current = config2.get("current");
|
|
2192
|
+
const table = new Table3({
|
|
2193
|
+
head: ["Name", "Base URL", "Model"],
|
|
2194
|
+
style: { head: ["cyan"] }
|
|
2195
|
+
});
|
|
2196
|
+
Object.keys(registries).forEach((name) => {
|
|
2197
|
+
const reg = registries[name];
|
|
2198
|
+
const prefix = name === current ? chalk7.green("* ") : " ";
|
|
2199
|
+
table.push([
|
|
2200
|
+
prefix + name,
|
|
2201
|
+
reg.ANTHROPIC_BASE_URL || "-",
|
|
2202
|
+
reg.ANTHROPIC_MODEL || "-"
|
|
2203
|
+
]);
|
|
2204
|
+
});
|
|
2205
|
+
console.log(table.toString());
|
|
2206
|
+
});
|
|
2207
|
+
program.command("use <name>").description("Switch to a specific environment").action(async (name) => {
|
|
2208
|
+
await switchEnvironment(name);
|
|
2209
|
+
});
|
|
2210
|
+
program.command("add <name>").description("Add a new environment configuration").action(async (name) => {
|
|
2211
|
+
const registries = config2.get("registries");
|
|
2212
|
+
if (registries[name]) {
|
|
2213
|
+
console.log(chalk7.red(`Environment '${name}' already exists.`));
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
const { usePreset } = await inquirer.prompt([
|
|
2217
|
+
{
|
|
2218
|
+
type: "confirm",
|
|
2219
|
+
name: "usePreset",
|
|
2220
|
+
message: "Do you want to use a preset configuration?",
|
|
2221
|
+
default: true
|
|
2222
|
+
}
|
|
2223
|
+
]);
|
|
2224
|
+
let presetConfig = {};
|
|
2225
|
+
if (usePreset) {
|
|
2226
|
+
const { presetName } = await inquirer.prompt([
|
|
2227
|
+
{
|
|
2228
|
+
type: "list",
|
|
2229
|
+
name: "presetName",
|
|
2230
|
+
message: "Select a preset:",
|
|
2231
|
+
choices: Object.keys(ENV_PRESETS)
|
|
2232
|
+
}
|
|
2233
|
+
]);
|
|
2234
|
+
presetConfig = ENV_PRESETS[presetName];
|
|
2235
|
+
}
|
|
2236
|
+
const answers = await inquirer.prompt([
|
|
2237
|
+
{
|
|
2238
|
+
type: "input",
|
|
2239
|
+
name: "ANTHROPIC_BASE_URL",
|
|
2240
|
+
message: "Enter ANTHROPIC_BASE_URL:",
|
|
2241
|
+
default: presetConfig.ANTHROPIC_BASE_URL || "https://api.anthropic.com"
|
|
2242
|
+
},
|
|
2243
|
+
{
|
|
2244
|
+
type: "password",
|
|
2245
|
+
name: "ANTHROPIC_API_KEY",
|
|
2246
|
+
message: "Enter ANTHROPIC_API_KEY:"
|
|
2247
|
+
},
|
|
2248
|
+
{
|
|
2249
|
+
type: "input",
|
|
2250
|
+
name: "ANTHROPIC_MODEL",
|
|
2251
|
+
message: "Enter ANTHROPIC_MODEL:",
|
|
2252
|
+
default: presetConfig.ANTHROPIC_MODEL || "claude-sonnet-4-5-20250929"
|
|
2253
|
+
},
|
|
2254
|
+
{
|
|
2255
|
+
type: "input",
|
|
2256
|
+
name: "ANTHROPIC_SMALL_FAST_MODEL",
|
|
2257
|
+
message: "Enter ANTHROPIC_SMALL_FAST_MODEL:",
|
|
2258
|
+
default: presetConfig.ANTHROPIC_SMALL_FAST_MODEL || "claude-haiku-4-5-20251001"
|
|
2259
|
+
}
|
|
2260
|
+
]);
|
|
2261
|
+
if (answers.ANTHROPIC_API_KEY) {
|
|
2262
|
+
answers.ANTHROPIC_API_KEY = encrypt2(answers.ANTHROPIC_API_KEY);
|
|
2263
|
+
}
|
|
2264
|
+
registries[name] = answers;
|
|
2265
|
+
config2.set("registries", registries);
|
|
2266
|
+
console.log(chalk7.green(`Environment '${name}' added successfully.`));
|
|
2267
|
+
});
|
|
2268
|
+
program.command("del <name>").description("Delete an environment configuration").action((name) => {
|
|
2269
|
+
const registries = config2.get("registries");
|
|
2270
|
+
if (!registries[name]) {
|
|
2271
|
+
console.log(chalk7.red(`Environment '${name}' not found.`));
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
if (name === "official") {
|
|
2275
|
+
console.log(chalk7.red(`Cannot delete default 'official' environment.`));
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
delete registries[name];
|
|
2279
|
+
config2.set("registries", registries);
|
|
2280
|
+
const current = config2.get("current");
|
|
2281
|
+
if (current === name) {
|
|
2282
|
+
config2.set("current", "official");
|
|
2283
|
+
console.log(chalk7.yellow(`Deleted current environment. Switched back to 'official'.`));
|
|
2284
|
+
}
|
|
2285
|
+
console.log(chalk7.green(`Environment '${name}' deleted.`));
|
|
2286
|
+
});
|
|
2287
|
+
program.command("rename <old> <new>").description("Rename an environment configuration").action((oldName, newName) => {
|
|
2288
|
+
const registries = config2.get("registries");
|
|
2289
|
+
if (!registries[oldName]) {
|
|
2290
|
+
console.log(chalk7.red(`Environment '${oldName}' not found.`));
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
if (registries[newName]) {
|
|
2294
|
+
console.log(chalk7.red(`Environment '${newName}' already exists.`));
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
if (oldName === "official") {
|
|
2298
|
+
console.log(chalk7.red(`Cannot rename default 'official' environment.`));
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
registries[newName] = registries[oldName];
|
|
2302
|
+
delete registries[oldName];
|
|
2303
|
+
config2.set("registries", registries);
|
|
2304
|
+
const current = config2.get("current");
|
|
2305
|
+
if (current === oldName) {
|
|
2306
|
+
config2.set("current", newName);
|
|
2307
|
+
}
|
|
2308
|
+
console.log(chalk7.green(`Environment '${oldName}' renamed to '${newName}'.`));
|
|
2309
|
+
});
|
|
2310
|
+
program.command("cp <source> <target>").description("Copy an environment configuration").action(async (source, target) => {
|
|
2311
|
+
const registries = config2.get("registries");
|
|
2312
|
+
if (!registries[source]) {
|
|
2313
|
+
console.log(chalk7.red(`Environment '${source}' not found.`));
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
if (registries[target]) {
|
|
2317
|
+
console.log(chalk7.red(`Environment '${target}' already exists.`));
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
registries[target] = { ...registries[source] };
|
|
2321
|
+
config2.set("registries", registries);
|
|
2322
|
+
console.log(chalk7.green(`Environment '${source}' copied to '${target}'.`));
|
|
2323
|
+
const { modify } = await inquirer.prompt([
|
|
2324
|
+
{
|
|
2325
|
+
type: "confirm",
|
|
2326
|
+
name: "modify",
|
|
2327
|
+
message: "Do you want to modify the configuration?",
|
|
2328
|
+
default: false
|
|
2329
|
+
}
|
|
2330
|
+
]);
|
|
2331
|
+
if (modify) {
|
|
2332
|
+
const current = registries[target];
|
|
2333
|
+
const answers = await inquirer.prompt([
|
|
2334
|
+
{
|
|
2335
|
+
type: "input",
|
|
2336
|
+
name: "ANTHROPIC_BASE_URL",
|
|
2337
|
+
message: "ANTHROPIC_BASE_URL:",
|
|
2338
|
+
default: current.ANTHROPIC_BASE_URL
|
|
2339
|
+
},
|
|
2340
|
+
{
|
|
2341
|
+
type: "password",
|
|
2342
|
+
name: "ANTHROPIC_API_KEY",
|
|
2343
|
+
message: "ANTHROPIC_API_KEY (leave empty to keep current):"
|
|
2344
|
+
},
|
|
2345
|
+
{
|
|
2346
|
+
type: "input",
|
|
2347
|
+
name: "ANTHROPIC_MODEL",
|
|
2348
|
+
message: "ANTHROPIC_MODEL:",
|
|
2349
|
+
default: current.ANTHROPIC_MODEL
|
|
2350
|
+
},
|
|
2351
|
+
{
|
|
2352
|
+
type: "input",
|
|
2353
|
+
name: "ANTHROPIC_SMALL_FAST_MODEL",
|
|
2354
|
+
message: "ANTHROPIC_SMALL_FAST_MODEL:",
|
|
2355
|
+
default: current.ANTHROPIC_SMALL_FAST_MODEL
|
|
2356
|
+
}
|
|
2357
|
+
]);
|
|
2358
|
+
if (answers.ANTHROPIC_BASE_URL) current.ANTHROPIC_BASE_URL = answers.ANTHROPIC_BASE_URL;
|
|
2359
|
+
if (answers.ANTHROPIC_API_KEY) current.ANTHROPIC_API_KEY = encrypt2(answers.ANTHROPIC_API_KEY);
|
|
2360
|
+
if (answers.ANTHROPIC_MODEL) current.ANTHROPIC_MODEL = answers.ANTHROPIC_MODEL;
|
|
2361
|
+
if (answers.ANTHROPIC_SMALL_FAST_MODEL) current.ANTHROPIC_SMALL_FAST_MODEL = answers.ANTHROPIC_SMALL_FAST_MODEL;
|
|
2362
|
+
registries[target] = current;
|
|
2363
|
+
config2.set("registries", registries);
|
|
2364
|
+
console.log(chalk7.green(`Environment '${target}' updated.`));
|
|
2365
|
+
}
|
|
2366
|
+
});
|
|
2367
|
+
program.command("current").description("Show current environment name").action(() => {
|
|
2368
|
+
const current = config2.get("current");
|
|
2369
|
+
console.log(chalk7.green(current));
|
|
2370
|
+
});
|
|
2371
|
+
program.command("env").description("Output environment variables for shell eval").option("--json", "Output as JSON").action((options) => {
|
|
2372
|
+
const registries = config2.get("registries");
|
|
2373
|
+
const current = config2.get("current");
|
|
2374
|
+
const env = registries[current];
|
|
2375
|
+
if (!env) return;
|
|
2376
|
+
const outputEnv = { ...env };
|
|
2377
|
+
if (outputEnv.ANTHROPIC_API_KEY) {
|
|
2378
|
+
outputEnv.ANTHROPIC_API_KEY = decrypt2(outputEnv.ANTHROPIC_API_KEY);
|
|
2379
|
+
}
|
|
2380
|
+
if (options.json) {
|
|
2381
|
+
console.log(JSON.stringify(outputEnv, null, 2));
|
|
2382
|
+
} else {
|
|
2383
|
+
if (outputEnv.ANTHROPIC_BASE_URL) console.log(`export ANTHROPIC_BASE_URL="${outputEnv.ANTHROPIC_BASE_URL}"`);
|
|
2384
|
+
if (outputEnv.ANTHROPIC_API_KEY) console.log(`export ANTHROPIC_API_KEY="${outputEnv.ANTHROPIC_API_KEY}"`);
|
|
2385
|
+
if (outputEnv.ANTHROPIC_MODEL) console.log(`export ANTHROPIC_MODEL="${outputEnv.ANTHROPIC_MODEL}"`);
|
|
2386
|
+
if (outputEnv.ANTHROPIC_SMALL_FAST_MODEL) console.log(`export ANTHROPIC_SMALL_FAST_MODEL="${outputEnv.ANTHROPIC_SMALL_FAST_MODEL}"`);
|
|
2387
|
+
}
|
|
2388
|
+
});
|
|
2389
|
+
program.command("run <command...>").description("Run a command with the current environment variables").action((command) => {
|
|
2390
|
+
const registries = config2.get("registries");
|
|
2391
|
+
const current = config2.get("current");
|
|
2392
|
+
const envConfig = registries[current];
|
|
2393
|
+
if (!envConfig) {
|
|
2394
|
+
console.error(chalk7.red("No environment configuration found."));
|
|
2395
|
+
process.exit(1);
|
|
2396
|
+
}
|
|
2397
|
+
const env = { ...process.env };
|
|
2398
|
+
if (envConfig.ANTHROPIC_BASE_URL) env.ANTHROPIC_BASE_URL = envConfig.ANTHROPIC_BASE_URL;
|
|
2399
|
+
if (envConfig.ANTHROPIC_API_KEY) env.ANTHROPIC_API_KEY = decrypt2(envConfig.ANTHROPIC_API_KEY || "");
|
|
2400
|
+
if (envConfig.ANTHROPIC_MODEL) env.ANTHROPIC_MODEL = envConfig.ANTHROPIC_MODEL;
|
|
2401
|
+
if (envConfig.ANTHROPIC_SMALL_FAST_MODEL) env.ANTHROPIC_SMALL_FAST_MODEL = envConfig.ANTHROPIC_SMALL_FAST_MODEL;
|
|
2402
|
+
const [cmd, ...args] = command;
|
|
2403
|
+
const child = spawn3(cmd, args, {
|
|
2404
|
+
env,
|
|
2405
|
+
stdio: "inherit",
|
|
2406
|
+
shell: true
|
|
2407
|
+
});
|
|
2408
|
+
child.on("exit", (code) => {
|
|
2409
|
+
process.exit(code ?? 0);
|
|
2410
|
+
});
|
|
2411
|
+
});
|
|
2412
|
+
var setupCmd = program.command("setup").description("Setup commands for permanent configurations");
|
|
2413
|
+
setupCmd.command("perms").description("\u6C38\u4E45\u914D\u7F6E\u6743\u9650\u6A21\u5F0F").option("--yolo", "\u5E94\u7528 YOLO \u6A21\u5F0F\uFF08\u5168\u90E8\u653E\u5F00\uFF09").option("--dev", "\u5E94\u7528\u5F00\u53D1\u6A21\u5F0F").option("--readonly", "\u5E94\u7528\u53EA\u8BFB\u6A21\u5F0F").option("--safe", "\u5E94\u7528\u5B89\u5168\u6A21\u5F0F").option("--ci", "\u5E94\u7528 CI/CD \u6A21\u5F0F").option("--audit", "\u5E94\u7528\u5BA1\u8BA1\u6A21\u5F0F").option("--reset", "\u91CD\u7F6E\u6743\u9650\u914D\u7F6E").action(function() {
|
|
2414
|
+
const options = this.opts();
|
|
2415
|
+
if (options.reset) {
|
|
2416
|
+
resetPermissions();
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
for (const mode of PERMISSION_MODES) {
|
|
2420
|
+
if (options[mode]) {
|
|
2421
|
+
applyPermissionMode(mode);
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
console.log(chalk7.yellow("\u8BF7\u6307\u5B9A\u4E00\u4E2A\u6743\u9650\u6A21\u5F0F\uFF0C\u4F8B\u5982: ccem setup perms --dev"));
|
|
2426
|
+
console.log(chalk7.gray("\u53EF\u7528\u6A21\u5F0F: " + PERMISSION_MODES.join(", ")));
|
|
2427
|
+
console.log(chalk7.gray("\u91CD\u7F6E\u6743\u9650: ccem setup perms --reset"));
|
|
2428
|
+
});
|
|
2429
|
+
setupCmd.command("default-mode").description("\u8BBE\u7F6E\u9ED8\u8BA4\u6743\u9650\u6A21\u5F0F").option("--yolo", "YOLO \u6A21\u5F0F").option("--dev", "\u5F00\u53D1\u6A21\u5F0F").option("--readonly", "\u53EA\u8BFB\u6A21\u5F0F").option("--safe", "\u5B89\u5168\u6A21\u5F0F").option("--ci", "CI/CD \u6A21\u5F0F").option("--audit", "\u5BA1\u8BA1\u6A21\u5F0F").option("--reset", "\u6E05\u9664\u9ED8\u8BA4\u6A21\u5F0F").action(function() {
|
|
2430
|
+
const options = this.opts();
|
|
2431
|
+
if (options.reset) {
|
|
2432
|
+
config2.set("defaultMode", null);
|
|
2433
|
+
console.log(chalk7.green("\u5DF2\u6E05\u9664\u9ED8\u8BA4\u6743\u9650\u6A21\u5F0F"));
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
for (const mode of PERMISSION_MODES) {
|
|
2437
|
+
if (options[mode]) {
|
|
2438
|
+
config2.set("defaultMode", mode);
|
|
2439
|
+
console.log(chalk7.green(`\u5DF2\u8BBE\u7F6E\u9ED8\u8BA4\u6743\u9650\u6A21\u5F0F: ${PERMISSION_PRESETS4[mode].name}`));
|
|
2440
|
+
console.log(chalk7.gray(`\u4E0B\u6B21\u542F\u52A8 ccem \u65F6\u5C06\u9ED8\u8BA4\u4F7F\u7528\u6B64\u6A21\u5F0F`));
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
const currentDefault = config2.get("defaultMode");
|
|
2445
|
+
if (currentDefault && PERMISSION_PRESETS4[currentDefault]) {
|
|
2446
|
+
console.log(chalk7.green(`\u5F53\u524D\u9ED8\u8BA4\u6A21\u5F0F: ${PERMISSION_PRESETS4[currentDefault].name}`));
|
|
2447
|
+
} else {
|
|
2448
|
+
console.log(chalk7.yellow("\u672A\u8BBE\u7F6E\u9ED8\u8BA4\u6743\u9650\u6A21\u5F0F"));
|
|
2449
|
+
}
|
|
2450
|
+
console.log(chalk7.gray("\n\u8BBE\u7F6E\u9ED8\u8BA4\u6A21\u5F0F: ccem setup default-mode --dev"));
|
|
2451
|
+
console.log(chalk7.gray("\u6E05\u9664\u9ED8\u8BA4\u6A21\u5F0F: ccem setup default-mode --reset"));
|
|
2452
|
+
console.log(chalk7.gray("\u53EF\u7528\u6A21\u5F0F: " + PERMISSION_MODES.join(", ")));
|
|
2453
|
+
});
|
|
2454
|
+
setupCmd.command("init").description("\u521D\u59CB\u5316 Claude Code \u5168\u5C40\u914D\u7F6E\uFF08\u8DF3\u8FC7 onboarding\u3001\u7981\u7528\u9065\u6D4B\u3001\u5B89\u88C5 MCP \u5DE5\u5177\uFF09").action(async () => {
|
|
2455
|
+
await runSetupInit();
|
|
2456
|
+
});
|
|
2457
|
+
setupCmd.command("migrate").description("\u8FC1\u79FB\u65E7\u7248\u914D\u7F6E\u5230 ~/.ccem/").option("--clean", "\u8FC1\u79FB\u540E\u5220\u9664\u65E7\u914D\u7F6E\u6587\u4EF6").option("--force", "\u5F3A\u5236\u91CD\u65B0\u8FC1\u79FB\uFF08\u8986\u76D6\u73B0\u6709\u914D\u7F6E\uFF09").action(async function() {
|
|
2458
|
+
const options = this.opts();
|
|
2459
|
+
const newConfigPath = getCcemConfigPath();
|
|
2460
|
+
const legacyConfigPath = getLegacyConfigPath();
|
|
2461
|
+
console.log(chalk7.cyan("\n\u{1F504} \u914D\u7F6E\u8FC1\u79FB\n"));
|
|
2462
|
+
if (!fs7.existsSync(legacyConfigPath)) {
|
|
2463
|
+
console.log(chalk7.yellow("\u672A\u627E\u5230\u65E7\u7248\u914D\u7F6E\u6587\u4EF6"));
|
|
2464
|
+
console.log(chalk7.gray(` \u65E7\u8DEF\u5F84: ${legacyConfigPath}`));
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
if (fs7.existsSync(newConfigPath) && !options.force) {
|
|
2468
|
+
console.log(chalk7.green("\u2713 \u914D\u7F6E\u5DF2\u5728\u65B0\u8DEF\u5F84"));
|
|
2469
|
+
console.log(chalk7.gray(` \u8DEF\u5F84: ${newConfigPath}`));
|
|
2470
|
+
console.log(chalk7.gray("\n\u4F7F\u7528 --force \u5F3A\u5236\u91CD\u65B0\u8FC1\u79FB"));
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
try {
|
|
2474
|
+
ensureCcemDir3();
|
|
2475
|
+
fs7.copyFileSync(legacyConfigPath, newConfigPath);
|
|
2476
|
+
console.log(chalk7.green("\u2713 \u914D\u7F6E\u5DF2\u8FC1\u79FB"));
|
|
2477
|
+
console.log(chalk7.gray(` \u4ECE: ${legacyConfigPath}`));
|
|
2478
|
+
console.log(chalk7.gray(` \u5230: ${newConfigPath}`));
|
|
2479
|
+
if (options.clean) {
|
|
2480
|
+
fs7.unlinkSync(legacyConfigPath);
|
|
2481
|
+
const legacyDir = path5.dirname(legacyConfigPath);
|
|
2482
|
+
try {
|
|
2483
|
+
fs7.rmdirSync(legacyDir);
|
|
2484
|
+
} catch {
|
|
2485
|
+
}
|
|
2486
|
+
console.log(chalk7.green("\u2713 \u5DF2\u5220\u9664\u65E7\u914D\u7F6E\u6587\u4EF6"));
|
|
2487
|
+
}
|
|
2488
|
+
} catch (err) {
|
|
2489
|
+
console.error(chalk7.red(`\u2717 \u8FC1\u79FB\u5931\u8D25: ${err}`));
|
|
2490
|
+
}
|
|
2491
|
+
});
|
|
2492
|
+
setupCmd.command("cron").description("\u5B89\u88C5 ccem-cron skill \u5230 Claude Code\uFF08~/.claude/skills/\uFF09").option("--force", "\u5F3A\u5236\u8986\u76D6\u5DF2\u6709\u6587\u4EF6").action(async function() {
|
|
2493
|
+
const options = this.opts();
|
|
2494
|
+
const skillDir = path5.join(process.env.HOME || "~", ".claude", "skills");
|
|
2495
|
+
const targetPath = path5.join(skillDir, "ccem-cron.md");
|
|
2496
|
+
if (!fs7.existsSync(skillDir)) {
|
|
2497
|
+
fs7.mkdirSync(skillDir, { recursive: true });
|
|
2498
|
+
console.log(chalk7.gray(`\u521B\u5EFA\u76EE\u5F55: ${skillDir}`));
|
|
2499
|
+
}
|
|
2500
|
+
if (fs7.existsSync(targetPath) && !options.force) {
|
|
2501
|
+
const { overwrite } = await inquirer.prompt([
|
|
2502
|
+
{
|
|
2503
|
+
type: "confirm",
|
|
2504
|
+
name: "overwrite",
|
|
2505
|
+
message: `${targetPath} \u5DF2\u5B58\u5728\uFF0C\u662F\u5426\u8986\u76D6\uFF1F`,
|
|
2506
|
+
default: false
|
|
2507
|
+
}
|
|
2508
|
+
]);
|
|
2509
|
+
if (!overwrite) {
|
|
2510
|
+
console.log(chalk7.yellow("\u5DF2\u53D6\u6D88"));
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
fs7.writeFileSync(targetPath, CCEM_CRON_SKILL_CONTENT, "utf-8");
|
|
2515
|
+
console.log(chalk7.green(`\u2713 \u5DF2\u5B89\u88C5 ccem-cron skill`));
|
|
2516
|
+
console.log(chalk7.gray(` \u8DEF\u5F84: ${targetPath}`));
|
|
2517
|
+
console.log(chalk7.cyan(`
|
|
2518
|
+
\u5728 Claude Code \u4E2D\u4F7F\u7528 /ccem-cron \u5373\u53EF\u8C03\u7528\u6B64 skill`));
|
|
2519
|
+
});
|
|
2520
|
+
var skillCmd = program.command("skill").description("\u7BA1\u7406 Claude Code skills");
|
|
2521
|
+
skillCmd.command("add [url]").description("\u6DFB\u52A0 skill\uFF08\u4ECE\u5B98\u65B9\u9884\u8BBE\u6216 GitHub URL\uFF09").action(async (url) => {
|
|
2522
|
+
if (url) {
|
|
2523
|
+
addSkillFromGitHub(url);
|
|
2524
|
+
} else {
|
|
2525
|
+
const result = await runSkillSelector();
|
|
2526
|
+
if (result.type === "cancelled") {
|
|
2527
|
+
console.log(chalk7.yellow("\u5DF2\u53D6\u6D88"));
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
if (result.type === "custom") {
|
|
2531
|
+
const { customUrl } = await inquirer.prompt([
|
|
2532
|
+
{
|
|
2533
|
+
type: "input",
|
|
2534
|
+
name: "customUrl",
|
|
2535
|
+
message: "\u8F93\u5165 GitHub URL:",
|
|
2536
|
+
validate: (input) => {
|
|
2537
|
+
if (!input.trim()) return "\u8BF7\u8F93\u5165\u6709\u6548\u7684 URL";
|
|
2538
|
+
if (!input.includes("github.com") && !/^[\w-]+\/[\w-]+$/.test(input)) {
|
|
2539
|
+
return "\u8BF7\u8F93\u5165\u6709\u6548\u7684 GitHub URL \u6216 owner/repo \u683C\u5F0F";
|
|
2540
|
+
}
|
|
2541
|
+
return true;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
]);
|
|
2545
|
+
addSkillFromGitHub(customUrl);
|
|
2546
|
+
} else if (result.skill) {
|
|
2547
|
+
installSkill(result.skill);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
});
|
|
2551
|
+
skillCmd.command("ls").description("\u5217\u51FA\u5DF2\u5B89\u88C5\u7684 skills").action(() => {
|
|
2552
|
+
const skills = listInstalledSkills();
|
|
2553
|
+
if (skills.length === 0) {
|
|
2554
|
+
console.log(chalk7.yellow("\u5F53\u524D\u76EE\u5F55\u6CA1\u6709\u5B89\u88C5\u4EFB\u4F55 skill"));
|
|
2555
|
+
console.log(chalk7.gray("\u4F7F\u7528 ccem skill add \u6DFB\u52A0 skills"));
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
const table = new Table3({
|
|
2559
|
+
head: ["Name", "Path"],
|
|
2560
|
+
style: { head: ["cyan"] }
|
|
2561
|
+
});
|
|
2562
|
+
skills.forEach((skill) => {
|
|
2563
|
+
table.push([chalk7.green(skill.name), chalk7.gray(skill.path)]);
|
|
2564
|
+
});
|
|
2565
|
+
console.log(table.toString());
|
|
2566
|
+
});
|
|
2567
|
+
skillCmd.command("rm <name>").description("\u5220\u9664\u5DF2\u5B89\u88C5\u7684 skill").action((name) => {
|
|
2568
|
+
removeSkill(name);
|
|
2569
|
+
});
|
|
2570
|
+
program.command("load <url>").description("\u4ECE\u8FDC\u7A0B\u670D\u52A1\u5668\u52A0\u8F7D\u73AF\u5883\u914D\u7F6E").requiredOption("--secret <secret>", "\u89E3\u5BC6\u5BC6\u94A5").action(async (url, options) => {
|
|
2571
|
+
await loadFromRemote(url, options.secret);
|
|
2572
|
+
});
|
|
2573
|
+
program.command("launch").description(false).option("--env <name>", "\u73AF\u5883\u540D\u79F0").option("--perm <mode>", "\u6743\u9650\u6A21\u5F0F").option("--session-id <id>", "\u4F1A\u8BDD ID").option("--resume-session <id>", "\u6062\u590D\u4F1A\u8BDD ID").option("--working-dir <path>", "\u5DE5\u4F5C\u76EE\u5F55").action(async function() {
|
|
2574
|
+
const opts = this.opts();
|
|
2575
|
+
const envName = opts.env || config2.get("current");
|
|
2576
|
+
const registries = config2.get("registries");
|
|
2577
|
+
const envConfig = registries[envName];
|
|
2578
|
+
if (!envConfig) {
|
|
2579
|
+
console.error(chalk7.red(`Environment '${envName}' not found.`));
|
|
2580
|
+
process.exit(1);
|
|
2581
|
+
}
|
|
2582
|
+
await launchClaude({
|
|
2583
|
+
envConfig,
|
|
2584
|
+
permMode: opts.perm,
|
|
2585
|
+
workingDir: opts.workingDir,
|
|
2586
|
+
sessionId: opts.sessionId,
|
|
2587
|
+
resumeSessionId: opts.resumeSession,
|
|
2588
|
+
silent: true
|
|
2589
|
+
});
|
|
2590
|
+
});
|
|
2591
|
+
program.command("usage").description("\u663E\u793A\u4F7F\u7528\u7EDF\u8BA1\u5E76\u5237\u65B0\u7F13\u5B58").option("--json", "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA").action(async function() {
|
|
2592
|
+
const opts = this.opts();
|
|
2593
|
+
const stats = await getUsageStats();
|
|
2594
|
+
if (opts.json) {
|
|
2595
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
2596
|
+
} else {
|
|
2597
|
+
if (stats) {
|
|
2598
|
+
console.log(renderUsageDetail(stats));
|
|
2599
|
+
} else {
|
|
2600
|
+
console.log(chalk7.yellow("No usage data available"));
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
});
|
|
2604
|
+
program.action(async (options) => {
|
|
2605
|
+
if (options.mode) {
|
|
2606
|
+
showCurrentMode();
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
if (options.listModes) {
|
|
2610
|
+
listAvailableModes();
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
initUsageStats();
|
|
2614
|
+
while (true) {
|
|
2615
|
+
console.clear();
|
|
2616
|
+
showCurrentEnv(usageStats, usageLoading);
|
|
2617
|
+
console.log("");
|
|
2618
|
+
const defaultMode = config2.get("defaultMode");
|
|
2619
|
+
const registries = config2.get("registries");
|
|
2620
|
+
const current = config2.get("current");
|
|
2621
|
+
const envConfig = registries[current];
|
|
2622
|
+
const { action } = await inquirer.prompt([
|
|
2623
|
+
{
|
|
2624
|
+
type: "list",
|
|
2625
|
+
name: "action",
|
|
2626
|
+
message: chalk7.gray("Select action"),
|
|
2627
|
+
choices: getMainMenuChoices(defaultMode),
|
|
2628
|
+
prefix: chalk7.gray("?")
|
|
2629
|
+
}
|
|
2630
|
+
]);
|
|
2631
|
+
if (action === "start") {
|
|
2632
|
+
if (usageAbortController) {
|
|
2633
|
+
usageAbortController.abort();
|
|
2634
|
+
usageAbortController = null;
|
|
2635
|
+
}
|
|
2636
|
+
stopSpinner();
|
|
2637
|
+
if (!envConfig) {
|
|
2638
|
+
msg.error("No environment configuration found.");
|
|
2639
|
+
process.exit(1);
|
|
2640
|
+
}
|
|
2641
|
+
await launchClaude({ envConfig, permMode: defaultMode || void 0 });
|
|
2642
|
+
return;
|
|
2643
|
+
} else if (action === "usage") {
|
|
2644
|
+
console.clear();
|
|
2645
|
+
if (usageStats) {
|
|
2646
|
+
console.log(renderUsageDetail(usageStats));
|
|
2647
|
+
} else {
|
|
2648
|
+
msg.warning("No usage data available");
|
|
2649
|
+
}
|
|
2650
|
+
await inquirer.prompt([
|
|
2651
|
+
{
|
|
2652
|
+
type: "input",
|
|
2653
|
+
name: "continue",
|
|
2654
|
+
message: chalk7.gray("Press Enter to continue..."),
|
|
2655
|
+
prefix: ""
|
|
2656
|
+
}
|
|
2657
|
+
]);
|
|
2658
|
+
} else if (action === "switch") {
|
|
2659
|
+
const result = await selectEnvWithKeys(registries, current);
|
|
2660
|
+
if (result.action === "select") {
|
|
2661
|
+
config2.set("current", result.name);
|
|
2662
|
+
} else if (result.action === "edit") {
|
|
2663
|
+
const envToEdit = registries[result.name];
|
|
2664
|
+
console.log(chalk7.yellow(`
|
|
2665
|
+
Editing environment '${result.name}'`));
|
|
2666
|
+
const answers = await inquirer.prompt([
|
|
2667
|
+
{
|
|
2668
|
+
type: "input",
|
|
2669
|
+
name: "ANTHROPIC_BASE_URL",
|
|
2670
|
+
message: "ANTHROPIC_BASE_URL:",
|
|
2671
|
+
default: envToEdit.ANTHROPIC_BASE_URL
|
|
2672
|
+
},
|
|
2673
|
+
{
|
|
2674
|
+
type: "password",
|
|
2675
|
+
name: "ANTHROPIC_API_KEY",
|
|
2676
|
+
message: "ANTHROPIC_API_KEY (leave empty to keep current):"
|
|
2677
|
+
},
|
|
2678
|
+
{
|
|
2679
|
+
type: "input",
|
|
2680
|
+
name: "ANTHROPIC_MODEL",
|
|
2681
|
+
message: "ANTHROPIC_MODEL:",
|
|
2682
|
+
default: envToEdit.ANTHROPIC_MODEL
|
|
2683
|
+
},
|
|
2684
|
+
{
|
|
2685
|
+
type: "input",
|
|
2686
|
+
name: "ANTHROPIC_SMALL_FAST_MODEL",
|
|
2687
|
+
message: "ANTHROPIC_SMALL_FAST_MODEL:",
|
|
2688
|
+
default: envToEdit.ANTHROPIC_SMALL_FAST_MODEL
|
|
2689
|
+
}
|
|
2690
|
+
]);
|
|
2691
|
+
if (answers.ANTHROPIC_BASE_URL) envToEdit.ANTHROPIC_BASE_URL = answers.ANTHROPIC_BASE_URL;
|
|
2692
|
+
if (answers.ANTHROPIC_API_KEY) envToEdit.ANTHROPIC_API_KEY = encrypt2(answers.ANTHROPIC_API_KEY);
|
|
2693
|
+
if (answers.ANTHROPIC_MODEL) envToEdit.ANTHROPIC_MODEL = answers.ANTHROPIC_MODEL;
|
|
2694
|
+
if (answers.ANTHROPIC_SMALL_FAST_MODEL) envToEdit.ANTHROPIC_SMALL_FAST_MODEL = answers.ANTHROPIC_SMALL_FAST_MODEL;
|
|
2695
|
+
registries[result.name] = envToEdit;
|
|
2696
|
+
config2.set("registries", registries);
|
|
2697
|
+
msg.success(`Environment '${result.name}' updated.`);
|
|
2698
|
+
await new Promise((resolve2) => setTimeout(resolve2, 800));
|
|
2699
|
+
} else if (result.action === "rename") {
|
|
2700
|
+
if (result.name === "official") {
|
|
2701
|
+
msg.error("Cannot rename default 'official' environment.");
|
|
2702
|
+
await new Promise((resolve2) => setTimeout(resolve2, 800));
|
|
2703
|
+
} else {
|
|
2704
|
+
const { newName } = await inquirer.prompt([
|
|
2705
|
+
{
|
|
2706
|
+
type: "input",
|
|
2707
|
+
name: "newName",
|
|
2708
|
+
message: `Rename '${result.name}' to:`,
|
|
2709
|
+
validate: (input) => {
|
|
2710
|
+
if (!input.trim()) return "Name cannot be empty";
|
|
2711
|
+
if (registries[input]) return `Environment '${input}' already exists`;
|
|
2712
|
+
return true;
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
]);
|
|
2716
|
+
registries[newName] = registries[result.name];
|
|
2717
|
+
delete registries[result.name];
|
|
2718
|
+
config2.set("registries", registries);
|
|
2719
|
+
if (current === result.name) {
|
|
2720
|
+
config2.set("current", newName);
|
|
2721
|
+
}
|
|
2722
|
+
msg.success(`Environment '${result.name}' renamed to '${newName}'.`);
|
|
2723
|
+
await new Promise((resolve2) => setTimeout(resolve2, 800));
|
|
2724
|
+
}
|
|
2725
|
+
} else if (result.action === "copy") {
|
|
2726
|
+
const { targetName } = await inquirer.prompt([
|
|
2727
|
+
{
|
|
2728
|
+
type: "input",
|
|
2729
|
+
name: "targetName",
|
|
2730
|
+
message: `Copy '${result.name}' to:`,
|
|
2731
|
+
validate: (input) => {
|
|
2732
|
+
if (!input.trim()) return "Name cannot be empty";
|
|
2733
|
+
if (registries[input]) return `Environment '${input}' already exists`;
|
|
2734
|
+
return true;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
]);
|
|
2738
|
+
registries[targetName] = { ...registries[result.name] };
|
|
2739
|
+
config2.set("registries", registries);
|
|
2740
|
+
msg.success(`Environment '${result.name}' copied to '${targetName}'.`);
|
|
2741
|
+
const { modify } = await inquirer.prompt([
|
|
2742
|
+
{
|
|
2743
|
+
type: "confirm",
|
|
2744
|
+
name: "modify",
|
|
2745
|
+
message: "Do you want to modify the configuration?",
|
|
2746
|
+
default: false
|
|
2747
|
+
}
|
|
2748
|
+
]);
|
|
2749
|
+
if (modify) {
|
|
2750
|
+
const envToEdit = registries[targetName];
|
|
2751
|
+
const editAnswers = await inquirer.prompt([
|
|
2752
|
+
{
|
|
2753
|
+
type: "input",
|
|
2754
|
+
name: "ANTHROPIC_BASE_URL",
|
|
2755
|
+
message: "ANTHROPIC_BASE_URL:",
|
|
2756
|
+
default: envToEdit.ANTHROPIC_BASE_URL
|
|
2757
|
+
},
|
|
2758
|
+
{
|
|
2759
|
+
type: "password",
|
|
2760
|
+
name: "ANTHROPIC_API_KEY",
|
|
2761
|
+
message: "ANTHROPIC_API_KEY (leave empty to keep current):"
|
|
2762
|
+
},
|
|
2763
|
+
{
|
|
2764
|
+
type: "input",
|
|
2765
|
+
name: "ANTHROPIC_MODEL",
|
|
2766
|
+
message: "ANTHROPIC_MODEL:",
|
|
2767
|
+
default: envToEdit.ANTHROPIC_MODEL
|
|
2768
|
+
},
|
|
2769
|
+
{
|
|
2770
|
+
type: "input",
|
|
2771
|
+
name: "ANTHROPIC_SMALL_FAST_MODEL",
|
|
2772
|
+
message: "ANTHROPIC_SMALL_FAST_MODEL:",
|
|
2773
|
+
default: envToEdit.ANTHROPIC_SMALL_FAST_MODEL
|
|
2774
|
+
}
|
|
2775
|
+
]);
|
|
2776
|
+
if (editAnswers.ANTHROPIC_BASE_URL) envToEdit.ANTHROPIC_BASE_URL = editAnswers.ANTHROPIC_BASE_URL;
|
|
2777
|
+
if (editAnswers.ANTHROPIC_API_KEY) envToEdit.ANTHROPIC_API_KEY = encrypt2(editAnswers.ANTHROPIC_API_KEY);
|
|
2778
|
+
if (editAnswers.ANTHROPIC_MODEL) envToEdit.ANTHROPIC_MODEL = editAnswers.ANTHROPIC_MODEL;
|
|
2779
|
+
if (editAnswers.ANTHROPIC_SMALL_FAST_MODEL) envToEdit.ANTHROPIC_SMALL_FAST_MODEL = editAnswers.ANTHROPIC_SMALL_FAST_MODEL;
|
|
2780
|
+
registries[targetName] = envToEdit;
|
|
2781
|
+
config2.set("registries", registries);
|
|
2782
|
+
msg.success(`Environment '${targetName}' updated.`);
|
|
2783
|
+
}
|
|
2784
|
+
await new Promise((resolve2) => setTimeout(resolve2, 800));
|
|
2785
|
+
} else if (result.action === "delete") {
|
|
2786
|
+
if (result.name === "official") {
|
|
2787
|
+
msg.error("Cannot delete default 'official' environment.");
|
|
2788
|
+
await new Promise((resolve2) => setTimeout(resolve2, 800));
|
|
2789
|
+
} else {
|
|
2790
|
+
const { confirm } = await inquirer.prompt([
|
|
2791
|
+
{
|
|
2792
|
+
type: "confirm",
|
|
2793
|
+
name: "confirm",
|
|
2794
|
+
message: `Are you sure you want to delete '${result.name}'?`,
|
|
2795
|
+
default: false
|
|
2796
|
+
}
|
|
2797
|
+
]);
|
|
2798
|
+
if (confirm) {
|
|
2799
|
+
delete registries[result.name];
|
|
2800
|
+
config2.set("registries", registries);
|
|
2801
|
+
if (current === result.name) {
|
|
2802
|
+
config2.set("current", "official");
|
|
2803
|
+
msg.warning(`Deleted current environment. Switched back to 'official'.`);
|
|
2804
|
+
} else {
|
|
2805
|
+
msg.success(`Environment '${result.name}' deleted.`);
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
await new Promise((resolve2) => setTimeout(resolve2, 800));
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
} else if (action === "perm") {
|
|
2812
|
+
const { permMode } = await inquirer.prompt([
|
|
2813
|
+
{
|
|
2814
|
+
type: "list",
|
|
2815
|
+
name: "permMode",
|
|
2816
|
+
message: chalk7.gray("Select permission mode"),
|
|
2817
|
+
choices: getPermModeChoices(null, false),
|
|
2818
|
+
prefix: chalk7.gray("?")
|
|
2819
|
+
}
|
|
2820
|
+
]);
|
|
2821
|
+
if (permMode !== "back") {
|
|
2822
|
+
await runWithTempPermissions(permMode, envConfig);
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
} else if (action === "setDefault") {
|
|
2826
|
+
const currentDefault = config2.get("defaultMode");
|
|
2827
|
+
const { selectedMode } = await inquirer.prompt([
|
|
2828
|
+
{
|
|
2829
|
+
type: "list",
|
|
2830
|
+
name: "selectedMode",
|
|
2831
|
+
message: chalk7.gray("Set default permission mode"),
|
|
2832
|
+
choices: getPermModeChoices(currentDefault, true),
|
|
2833
|
+
prefix: chalk7.gray("?")
|
|
2834
|
+
}
|
|
2835
|
+
]);
|
|
2836
|
+
if (selectedMode === "clear") {
|
|
2837
|
+
config2.set("defaultMode", null);
|
|
2838
|
+
msg.success("Default mode cleared");
|
|
2839
|
+
await new Promise((resolve2) => setTimeout(resolve2, 800));
|
|
2840
|
+
} else if (selectedMode !== "back") {
|
|
2841
|
+
config2.set("defaultMode", selectedMode);
|
|
2842
|
+
msg.success(`Default mode set: ${PERMISSION_PRESETS4[selectedMode].name}`);
|
|
2843
|
+
await new Promise((resolve2) => setTimeout(resolve2, 800));
|
|
2844
|
+
}
|
|
2845
|
+
} else {
|
|
2846
|
+
process.exit(0);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
});
|
|
2850
|
+
program.parse(process.argv);
|