@initx-plugin/cp 0.0.23 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -10
- package/dist/index.d.mts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.mjs +6 -1
- package/package.json +15 -15
package/README.md
CHANGED
|
@@ -6,34 +6,62 @@
|
|
|
6
6
|
|
|
7
7
|
## @initx-plugin/cp
|
|
8
8
|
|
|
9
|
-
Copy plugin for `initx
|
|
9
|
+
Copy plugin for `initx`. Ships with built-in presets (SSH / GPG / CWD) and an optional GitHub-backed data store so you can copy private snippets from anywhere.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx initx plugin add cp
|
|
13
|
+
```
|
|
10
14
|
|
|
11
15
|
## Usage
|
|
12
16
|
|
|
17
|
+
### Built-in presets
|
|
18
|
+
|
|
13
19
|
```bash
|
|
14
|
-
|
|
20
|
+
npx initx cp cwd # current working directory
|
|
21
|
+
npx initx cp ssh # SSH public key (picks from .ssh/ or interactively)
|
|
22
|
+
npx initx cp gpg # GPG public key
|
|
15
23
|
```
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
### GitHub-backed data store
|
|
26
|
+
|
|
27
|
+
Point `cp` at a private GitHub repo and a personal access token, then store named snippets as files. Any `cp <key>` that is not a built-in preset will look up the file from the repo and copy it to your clipboard. Files are cached locally under `~/.initx/cp/cache/`.
|
|
18
28
|
|
|
19
|
-
|
|
29
|
+
#### 1. Configure the store
|
|
30
|
+
|
|
31
|
+
Either walk through an interactive setup or set values one by one:
|
|
20
32
|
|
|
21
33
|
```bash
|
|
22
|
-
npx initx cp
|
|
34
|
+
npx initx cp-config setup
|
|
35
|
+
# or
|
|
36
|
+
npx initx cp-config set repo owner/private-repo
|
|
37
|
+
npx initx cp-config set token ghp_xxxxxxxxxxxxxxxxxxxx
|
|
38
|
+
# optional
|
|
39
|
+
npx initx cp-config set branch main # default: main
|
|
40
|
+
npx initx cp-config set path data # path inside the repo
|
|
23
41
|
```
|
|
24
42
|
|
|
25
|
-
|
|
43
|
+
`cp-config setup` prompts for each value in turn. Press Enter to keep the current value, type to overwrite. The token prompt is always masked.
|
|
44
|
+
|
|
45
|
+
Token is saved in plaintext at `~/.initx/cp/config.json`. Prefer a fine-grained PAT scoped to only the target repository.
|
|
46
|
+
|
|
47
|
+
#### 2. Add / fetch / remove data keys
|
|
26
48
|
|
|
27
49
|
```bash
|
|
28
|
-
npx initx cp
|
|
50
|
+
npx initx cp-config set my-token "abc 123"
|
|
51
|
+
npx initx cp my-token # copies "abc 123" to clipboard
|
|
52
|
+
npx initx cp-config get my-token
|
|
53
|
+
npx initx cp-config list
|
|
54
|
+
npx initx cp-config rm my-token
|
|
29
55
|
```
|
|
30
56
|
|
|
31
|
-
|
|
57
|
+
Each key becomes a file at `<path>/<key>` inside the repo, with the value as the file content. Keys cannot contain `/` and may not be `.` or `..`. On `ix cp <key>`, the cached value is used when available; otherwise the file is fetched from GitHub and cached.
|
|
58
|
+
|
|
59
|
+
#### 3. Inspect config
|
|
32
60
|
|
|
33
61
|
```bash
|
|
34
|
-
npx initx cp
|
|
62
|
+
npx initx cp-config status # shows config (token redacted)
|
|
35
63
|
```
|
|
36
64
|
|
|
37
65
|
## Documentation
|
|
38
66
|
|
|
39
|
-
[initx](https://github.com/initx-collective/initx)
|
|
67
|
+
[initx](https://github.com/initx-collective/initx)
|
package/dist/index.d.mts
CHANGED
|
@@ -8,11 +8,23 @@ declare enum CpType {
|
|
|
8
8
|
|
|
9
9
|
declare class CpPlugin extends InitxPlugin {
|
|
10
10
|
rules: InitxMatcherRules;
|
|
11
|
-
handle(
|
|
11
|
+
handle(ctx: InitxContext, ...args: string[]): Promise<void>;
|
|
12
|
+
private handleCopy;
|
|
13
|
+
private runPreset;
|
|
14
|
+
private runDataCopy;
|
|
15
|
+
private runList;
|
|
12
16
|
[CpType.SSH](): Promise<void>;
|
|
13
17
|
[CpType.GPG](): Promise<void>;
|
|
14
18
|
[CpType.CWD](): Promise<void>;
|
|
15
19
|
private copy;
|
|
20
|
+
private handleConfigCommand;
|
|
21
|
+
private configSet;
|
|
22
|
+
private configGet;
|
|
23
|
+
private configList;
|
|
24
|
+
private configRm;
|
|
25
|
+
private configStatus;
|
|
26
|
+
private configSetup;
|
|
27
|
+
private printConfigHelp;
|
|
16
28
|
}
|
|
17
29
|
|
|
18
30
|
export { CpPlugin as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -8,11 +8,23 @@ declare enum CpType {
|
|
|
8
8
|
|
|
9
9
|
declare class CpPlugin extends InitxPlugin {
|
|
10
10
|
rules: InitxMatcherRules;
|
|
11
|
-
handle(
|
|
11
|
+
handle(ctx: InitxContext, ...args: string[]): Promise<void>;
|
|
12
|
+
private handleCopy;
|
|
13
|
+
private runPreset;
|
|
14
|
+
private runDataCopy;
|
|
15
|
+
private runList;
|
|
12
16
|
[CpType.SSH](): Promise<void>;
|
|
13
17
|
[CpType.GPG](): Promise<void>;
|
|
14
18
|
[CpType.CWD](): Promise<void>;
|
|
15
19
|
private copy;
|
|
20
|
+
private handleConfigCommand;
|
|
21
|
+
private configSet;
|
|
22
|
+
private configGet;
|
|
23
|
+
private configList;
|
|
24
|
+
private configRm;
|
|
25
|
+
private configStatus;
|
|
26
|
+
private configSetup;
|
|
27
|
+
private printConfigHelp;
|
|
16
28
|
}
|
|
17
29
|
|
|
18
30
|
export { CpPlugin as default };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
-
import{existsSync as
|
|
1
|
+
import{existsSync as f,readFileSync as b,writeFileSync as x,mkdirSync as T,unlinkSync as _,readdirSync as q}from"node:fs";import B,{homedir as G}from"node:os";import{resolve as h}from"node:path";import L,{cwd as W}from"node:process";import{InitxPlugin as M}from"@initx-plugin/core";import{log as o,loadingFunction as g,inquirer as l,gpgList as z,c as X}from"@initx-plugin/utils";import Q from"clipboardy";import{Buffer as O}from"node:buffer";const S=h(G(),".initx","cp"),E=h(S,"config.json");function Z(){return f(S)||T(S,{recursive:!0}),S}function y(){if(!f(E))return{};try{return JSON.parse(b(E,"utf8"))}catch{return{}}}function N(n){Z(),x(E,`${JSON.stringify(n,null,2)}
|
|
2
|
+
`,"utf8")}function tt(n){return y()[n]}function m(n,t){const e=y();e[n]=t,N(e)}function et(n){const t=y();delete t[n],N(t)}function k(){const n=y();if(!n.repo)throw new Error("cp-config repo is not set. Run `ix cp-config set repo <owner/repo>` first.");if(!n.token)throw new Error("cp-config token is not set. Run `ix cp-config set token <github_pat>` first.");return{repo:n.repo,token:n.token,branch:n.branch??"main",path:n.path??"data"}}const P=h(G(),".initx","cp","cache");function nt(){f(P)||T(P,{recursive:!0})}function H(n){return h(P,n)}function rt(n){const t=H(n);if(!f(t))return null;try{return JSON.parse(b(t,"utf8"))}catch{return null}}function ot(n,t,e){nt();const r={content:t,sha:e,fetchedAt:Date.now()};return x(H(n),JSON.stringify(r),"utf8"),r}function at(n){const t=H(n);f(t)&&_(t)}class w extends Error{constructor(t,e,r){super(`GitHub API error: ${t} ${e}${r?` - ${r}`:""}`),this.status=t,this.statusText=e,this.body=r,this.name="GitHubApiError"}}const it="initx-plugin-cp";function st(n){return n.split("/").map(encodeURIComponent).join("/")}class ct{constructor(t,e,r="main"){this.repo=t,this.token=e,this.branch=r,this.validateRepo(t)}validateRepo(t){if(!t.includes("/"))throw new Error(`Invalid repo format: "${t}". Expected "owner/repo".`)}apiUrl(t){const[e,r]=this.repo.split("/",2);return`https://api.github.com/repos/${encodeURIComponent(e)}/${encodeURIComponent(r)}/contents/${st(t)}?ref=${encodeURIComponent(this.branch)}`}headers(){return{Authorization:`Bearer ${this.token}`,Accept:"application/vnd.github+json","X-GitHub-Api-Version":"2022-11-28","User-Agent":it}}async getFile(t){const e=await fetch(this.apiUrl(t),{headers:this.headers()});if(e.status===404)return null;if(!e.ok)throw new w(e.status,e.statusText,await e.text());const r=await e.json();if(r.type!=="file")throw new w(e.status,e.statusText,`Path "${t}" is not a file.`);return r}async listDir(t){const e=await fetch(this.apiUrl(t),{headers:this.headers()});if(e.status===404)return[];if(!e.ok)throw new w(e.status,e.statusText,await e.text());const r=await e.json();if(!Array.isArray(r))throw new w(e.status,e.statusText,`Path "${t}" is not a directory.`);return r}async putFile(t,e,r,i){const a={message:r??`cp: update ${t}`,content:O.from(e,"utf8").toString("base64"),branch:this.branch,...i?{sha:i}:{}},s=await fetch(this.apiUrl(t),{method:"PUT",headers:{...this.headers(),"Content-Type":"application/json"},body:JSON.stringify(a)});if(!s.ok)throw new w(s.status,s.statusText,await s.text())}async deleteFile(t,e,r){const i={message:r??`cp: delete ${t}`,sha:e,branch:this.branch},a=await fetch(this.apiUrl(t),{method:"DELETE",headers:{...this.headers(),"Content-Type":"application/json"},body:JSON.stringify(i)});if(!a.ok)throw new w(a.status,a.statusText,await a.text())}decodeContent(t){const e=t.content.replace(/\n/g,"");return O.from(e,t.encoding||"base64").toString("utf8")}}function U(n,t){return C(n),`${t}/${n}`}function ut(n){for(let t=0;t<n.length;t++){const e=n.charCodeAt(t);if(e<32||e===127)return!0}return!1}function C(n){if(!n)throw new Error("Key cannot be empty.");if(n.includes("/"))throw new Error(`Key cannot contain / or "${n}". Nested paths are not supported.`);if(n==="."||n==="..")throw new Error(`Key cannot be "${n}".`);if(ut(n))throw new Error("Key cannot contain control characters.");if(n.length>200)throw new Error("Key is too long (max 200 characters).")}function $(){const n=k();return new ct(n.repo,n.token,n.branch)}async function I(n,t){C(n);const e=$(),r=k(),i=U(n,r.path);{const p=rt(n);if(p)return p.content}const a=await e.getFile(i);if(!a)return null;const s=e.decodeContent(a);return ot(n,s,a.sha),s}async function pt(n,t){C(n);const e=$(),r=k(),i=U(n,r.path),a=await e.getFile(i);await e.putFile(i,t,`cp: set ${n}`,a?.sha),at(n)}async function ft(n){C(n);const t=$(),e=k(),r=U(n,e.path),i=await t.getFile(r);i&&await t.deleteFile(r,i.sha,`cp: delete ${n}`)}async function J(){const n=$(),t=k();return(await n.listDir(t.path)).filter(e=>e.type==="file").map(e=>e.name)}const D=h(G(),".initx","cp"),A=h(D,"recent.json");function F(){if(!f(A))return{order:[]};try{const n=JSON.parse(b(A,"utf8"));return Array.isArray(n.order)?{order:n.order.filter(t=>typeof t=="string")}:{order:[]}}catch{return{order:[]}}}function K(n){f(D)||T(D,{recursive:!0}),x(A,`${JSON.stringify(n,null,2)}
|
|
3
|
+
`,"utf8")}function ht(){return F().order}function R(n){const t=F(),e=t.order.indexOf(n);e!==-1&&t.order.splice(e,1),t.order.unshift(n),K(t)}function lt(n){const t=F(),e=t.order.indexOf(n);e!==-1&&(t.order.splice(e,1),K(t))}var d=(n=>(n.SSH="ssh",n.GPG="gpg",n.CWD="cwd",n))(d||{}),u=(n=>(n.SET="set",n.GET="get",n.LIST="list",n.LS="ls",n.RM="rm",n.STATUS="status",n.SETUP="setup",n.HELP="help",n))(u||{});const dt=["repo","token","branch","path"],V=new Set(Object.values(d)),j=new Set(dt);class gt extends M{rules=[{matching:"cp",description:"Copy SSH / GPG / CWD / data key to clipboard"},{matching:"cp-config",description:"Configure cp plugin (repo, token, branch, path) and data keys"}];async handle(t,...e){if(t.optionsList.includes("--list")){await this.runList();return}if(t.key==="cp-config"){await this.handleConfigCommand(e);return}await this.handleCopy(e)}async handleCopy(t){const[e]=t;if(!e){o.error(`Please enter the copy type. Available presets: ${Object.values(d).join(", ")}`);return}if(V.has(e)){await this.runPreset(e);return}await this.runDataCopy(e)}async runPreset(t){const e=this[t];if(typeof e!="function"){o.error(`Unknown preset: ${t}`);return}await e.call(this),R(t)}async runDataCopy(t){try{const e=await g(`Fetching "${t}"`,()=>I(t));if(e===null){o.error(`Data key "${t}" not found. Use \`ix cp-config set ${t} <value>\` to create it.`);return}this.copy(e),o.success(`Data key "${t}" copied to clipboard`),R(t)}catch(e){o.error(`Failed to fetch "${t}": ${e.message}`)}}async runList(){if(!L.stdin.isTTY){o.error("cp --list requires an interactive TTY.");return}const t=Object.values(d);let e=[];try{e=await g("Loading data keys",()=>J())}catch(c){o.warn(`Could not load data keys: ${c.message}`)}const r=new Set([...t,...e]),i=ht().filter(c=>r.has(c)),a=new Set,s=[];for(const c of i)a.has(c)||(s.push(c),a.add(c));for(const c of[...e].sort())a.has(c)||(s.push(c),a.add(c));for(const c of t)a.has(c)||(s.push(c),a.add(c));if(s.length===0){o.warn("No copyable keys found.");return}const p=s.map(c=>{const Y=V.has(c);return{name:c,value:c,description:Y?"preset":"data"}}),v=await l.search("Select a key to copy",p);await this.handleCopy([v])}async[d.SSH](){const t=h(B.homedir(),".ssh");if(!f(t)){o.error(`SSH directory not found, path: ${t}`);return}const e=q(t).filter(s=>s.endsWith(".pub"));if(e.length===0){o.error("SSH key not found");return}let r;if(e.length===1){const[s]=e;r=s}else{const s=await l.select("Select SSH key",e);r=e[s]}const i=h(t,r),a=b(i,"utf8");this.copy(a),o.success("Key copied to clipboard")}async[d.GPG](){const t=await z();if(t.length===0){o.error("No GPG keys found");return}let e=0;t.length>1&&(e=await l.select("Select GPG key",t.map(({key:a,name:s,email:p})=>`${a} - ${s} <${p}>`)));const{key:r}=t[e],i=await X("gpg",["--armor","--export",r]);this.copy(i.content),o.success("GPG public key copied to clipboard")}async[d.CWD](){this.copy(W()),o.success("Current working directory copied to clipboard")}copy(t){Q.writeSync(t)}async handleConfigCommand(t){const[e,...r]=t;if(!e||u.HELP===e||e==="--help"||e==="-h"){this.printConfigHelp();return}switch(e){case u.SET:await this.configSet(r);break;case u.GET:await this.configGet(r[0]);break;case u.LIST:case u.LS:await this.configList();break;case u.RM:await this.configRm(r[0]);break;case u.STATUS:this.configStatus();break;case u.SETUP:await this.configSetup();break;default:o.error(`Unknown command: ${e}`),this.printConfigHelp()}}async configSet(t){const[e,...r]=t;if(!e){o.error("Usage: cp-config set <key> <value>");return}if(j.has(e)){const a=r.join(" ").trim();if(!a&&e!=="token"&&e!=="branch"&&e!=="path"){o.error(`Usage: cp-config set ${e} <value>`);return}e==="token"&&o.warn("Token is saved in plaintext at ~/.initx/cp/config.json. Prefer a fine-grained GitHub PAT with minimal scopes."),m(e,a||(e==="branch"?"main":e==="path"?"data":"")),o.success(`Config "${e}" saved`);return}const i=r.join(" ");if(!i){o.error("Usage: cp-config set <key> <value>");return}try{await g(`Saving "${e}" to GitHub`,()=>pt(e,i)),o.success(`Data "${e}" saved`),R(e)}catch(a){o.error(`Failed to save data: ${a.message}`)}}async configGet(t){if(!t){o.error("Usage: cp-config get <key>");return}if(j.has(t)){const e=tt(t);if(!e){o.warn(`Config "${t}" is not set`);return}console.log(e);return}try{const e=await g(`Fetching "${t}" from GitHub`,()=>I(t));if(e===null){o.error(`Data key "${t}" not found`);return}console.log(e)}catch(e){o.error(`Failed to get data: ${e.message}`)}}async configList(){try{const t=await g("Listing data keys",()=>J());if(t.length===0){o.info("No data keys configured");return}console.log(t.join(`
|
|
4
|
+
`))}catch(t){o.error(`Failed to list data: ${t.message}`)}}async configRm(t){if(!t){o.error("Usage: cp-config rm <key>");return}if(j.has(t)){et(t),o.success(`Config "${t}" removed`);return}try{await g(`Removing "${t}" from GitHub`,()=>ft(t)),o.success(`Data "${t}" removed`),lt(t)}catch(e){o.error(`Failed to remove data: ${e.message}`)}}configStatus(){const t={...y()};typeof t.token=="string"&&t.token&&(t.token="***"),console.log(JSON.stringify(t,null,2))}async configSetup(){if(!L.stdin.isTTY){o.error("cp-config setup requires an interactive TTY. Use `cp-config set <key> <value>` instead.");return}const t=y();o.info("cp-config setup \u2014 press Enter to keep the current value, type to overwrite.");try{const e=t.repo?`GitHub repo (current: ${t.repo})`:"GitHub repo (owner/repo)",r=await l.input(e,{default:t.repo??void 0,required:!0,pattern:/^[^/\s]+\/[^/\s]+$/,patternError:'Repo must be in the form "owner/repo".'});m("repo",r),o.success('Config "repo" saved');const i=t.token?"GitHub PAT (Enter to keep current value, or paste a new token)":"GitHub PAT",a=await l.password(i,{mask:"*"});a&&a!==t.token?(o.warn("Token is saved in plaintext at ~/.initx/cp/config.json. Prefer a fine-grained GitHub PAT with minimal scopes."),m("token",a),o.success('Config "token" saved')):!a&&t.token?o.info("Token unchanged."):!a&&!t.token&&o.warn("Token not set. You can set it later via `cp-config set token <value>`.");const s=t.branch?`Branch (current: ${t.branch})`:"Branch",p=await l.input(s,{default:t.branch||"main"});m("branch",p||"main"),o.success('Config "branch" saved');const v=t.path?`Path inside repo (current: ${t.path})`:"Path inside repo",c=await l.input(v,{default:t.path||"data"});m("path",c||"data"),o.success('Config "path" saved'),console.log(`
|
|
5
|
+
Current config:`),this.configStatus()}catch(e){o.warn(`Setup cancelled. ${e.message||""} Existing config left untouched.`)}}printConfigHelp(){console.log(["Usage: cp-config <command> [args]","","Commands:"," setup Interactive setup for repo / token / branch / path"," set <key> <value> Set a config value (repo, token, branch, path) or a data key"," get <key> Get a config or data value"," list, ls List data keys"," rm <key> Remove a config or data key"," status Show config (token redacted)"," help Show this help","","Config keys: repo, token, branch, path. Anything else is treated as a data key.","","Examples:"," cp-config setup"," cp-config set repo owner/private-repo"," cp-config set token ghp_xxxxxxxxxxxx",' cp-config set my-secret "hello world"'," cp-config get my-secret"," cp-config list"," cp-config rm my-secret"].join(`
|
|
6
|
+
`))}}export{gt as default};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@initx-plugin/cp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"description": "initx plugin, copy some text to clipboard",
|
|
6
6
|
"author": "imba97",
|
|
7
7
|
"license": "MIT",
|
|
@@ -28,25 +28,25 @@
|
|
|
28
28
|
"@initx-plugin/utils": "*"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"clipboardy": "^
|
|
31
|
+
"clipboardy": "^5.3.2"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@
|
|
35
|
-
"@initx-plugin/core": "
|
|
36
|
-
"@initx-plugin/utils": "
|
|
37
|
-
"@types/node": "^
|
|
38
|
-
"bumpp": "^
|
|
39
|
-
"eslint": "^
|
|
40
|
-
"
|
|
41
|
-
"simple-git-hooks": "^2.
|
|
42
|
-
"typescript": "
|
|
43
|
-
"unbuild": "^3.
|
|
44
|
-
"vitest": "^
|
|
34
|
+
"@antfu/eslint-config": "^9.5.1",
|
|
35
|
+
"@initx-plugin/core": "^0.4.1",
|
|
36
|
+
"@initx-plugin/utils": "^0.4.3",
|
|
37
|
+
"@types/node": "^26.6.4",
|
|
38
|
+
"bumpp": "^12.3.0",
|
|
39
|
+
"eslint": "^10.12.0",
|
|
40
|
+
"nano-staged": "^1.0.2",
|
|
41
|
+
"simple-git-hooks": "^2.14.0",
|
|
42
|
+
"typescript": "6.0.3",
|
|
43
|
+
"unbuild": "^3.6.1",
|
|
44
|
+
"vitest": "^5.0.3"
|
|
45
45
|
},
|
|
46
46
|
"simple-git-hooks": {
|
|
47
|
-
"pre-commit": "pnpm
|
|
47
|
+
"pre-commit": "pnpm nano-staged"
|
|
48
48
|
},
|
|
49
|
-
"
|
|
49
|
+
"nano-staged": {
|
|
50
50
|
"*": "eslint --cache --fix"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|