@codeshareme/codeshare-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +351 -0
- package/dist/api.d.ts +8 -0
- package/dist/api.js +30 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +42 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +277 -0
- package/dist/project.d.ts +8 -0
- package/dist/project.js +29 -0
- package/dist/scanner.d.ts +9 -0
- package/dist/scanner.js +82 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# codeshare-cli
|
|
2
|
+
|
|
3
|
+
> Official CLI for [codeshare.me](https://codeshare.me) — version-control your code snippets directly from the terminal.
|
|
4
|
+
|
|
5
|
+
Push, pull, diff and track the history of your CodeShare snippets just like you would with Git — without needing Git.
|
|
6
|
+
|
|
7
|
+
📖 **Full documentation:** [codeshare.me/docs/cli](https://codeshare.me/docs/cli)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Authentication](#authentication)
|
|
15
|
+
- [Quick Start](#quick-start)
|
|
16
|
+
- [Commands](#commands)
|
|
17
|
+
- [login](#login)
|
|
18
|
+
- [logout](#logout)
|
|
19
|
+
- [whoami](#whoami)
|
|
20
|
+
- [init](#init)
|
|
21
|
+
- [push](#push)
|
|
22
|
+
- [pull](#pull)
|
|
23
|
+
- [clone](#clone)
|
|
24
|
+
- [log](#log)
|
|
25
|
+
- [diff](#diff)
|
|
26
|
+
- [status](#status)
|
|
27
|
+
- [File Filtering](#file-filtering)
|
|
28
|
+
- [.csproject File](#csproject-file)
|
|
29
|
+
- [Limits](#limits)
|
|
30
|
+
- [Supported Languages](#supported-languages)
|
|
31
|
+
- [Requirements](#requirements)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install -g codeshare-cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Verify the installation:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cs --version
|
|
45
|
+
cs --help
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Authentication
|
|
51
|
+
|
|
52
|
+
1. Log in to [codeshare.me](https://codeshare.me) and go to **Settings → Personal Access Tokens**
|
|
53
|
+
2. Enter a name (e.g. `my-laptop`) and click **Generate Token**
|
|
54
|
+
3. **Copy the token immediately** — it is shown only once
|
|
55
|
+
4. Run:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cs login --token <your-token>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Your token is stored in `~/.codeshare/config.json` with `600` file permissions (readable only by you).
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Quick Start
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Authenticate
|
|
69
|
+
cs login --token cs_xxxxxxxxxxxxxxxxxxxxxx
|
|
70
|
+
|
|
71
|
+
# Go to your project folder
|
|
72
|
+
cd my-project
|
|
73
|
+
|
|
74
|
+
# Create a new snippet linked to this folder
|
|
75
|
+
cs init --title "My Awesome Project"
|
|
76
|
+
|
|
77
|
+
# Upload your files as version 1
|
|
78
|
+
cs push -m "initial commit"
|
|
79
|
+
|
|
80
|
+
# Make some changes, then push again
|
|
81
|
+
cs push -m "fix: handle edge case"
|
|
82
|
+
|
|
83
|
+
# See the version history
|
|
84
|
+
cs log
|
|
85
|
+
|
|
86
|
+
# See what changed between version 1 and 2
|
|
87
|
+
cs diff 1 2
|
|
88
|
+
|
|
89
|
+
# Check if local files are in sync with the latest version
|
|
90
|
+
cs status
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Commands
|
|
96
|
+
|
|
97
|
+
### `login`
|
|
98
|
+
|
|
99
|
+
Save your personal access token locally.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
cs login --token <token>
|
|
103
|
+
cs login --token <token> --host https://codeshare.me
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
| Option | Description |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `--token <token>` | Your personal access token (prompted interactively if omitted) |
|
|
109
|
+
| `--host <url>` | Override the API host (default: `https://codeshare.me`) |
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### `logout`
|
|
114
|
+
|
|
115
|
+
Remove the saved token from your machine.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
cs logout
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### `whoami`
|
|
124
|
+
|
|
125
|
+
Show who you are currently logged in as.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
cs whoami
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
Logged in as johndoe
|
|
133
|
+
Host: https://codeshare.me
|
|
134
|
+
Token: cs_abc123…
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### `init`
|
|
140
|
+
|
|
141
|
+
Create a new snippet on CodeShare and link the current directory to it. Writes a `.csproject` file in the current folder.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
cs init
|
|
145
|
+
cs init --title "React Authentication Flow"
|
|
146
|
+
cs init --title "My API" --private
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Option | Description |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `--title <title>` | Snippet title (prompted if omitted) |
|
|
152
|
+
| `--private` | Create as a private snippet (default: public) |
|
|
153
|
+
|
|
154
|
+
After running `init`, use `cs push` to upload your files.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### `push`
|
|
159
|
+
|
|
160
|
+
Scan the current directory, upload all files and create a new version snapshot.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
cs push
|
|
164
|
+
cs push -m "add error handling"
|
|
165
|
+
cs push --message "refactor: split into modules"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| Option | Description |
|
|
169
|
+
|---|---|
|
|
170
|
+
| `-m, --message <msg>` | Commit message (default: `"Update"`) |
|
|
171
|
+
|
|
172
|
+
**What gets uploaded:**
|
|
173
|
+
|
|
174
|
+
- All files in the current directory (recursively)
|
|
175
|
+
- Respects `.gitignore` and `.csignore`
|
|
176
|
+
- Skips: `node_modules`, `dist`, `build`, `.git`, `.env`, lock files, binary files
|
|
177
|
+
|
|
178
|
+
**Limits:** max 500 files, 512 KB per file, 50 MB total per push.
|
|
179
|
+
|
|
180
|
+
Each `push` creates a new immutable version. You can always go back.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### `pull`
|
|
185
|
+
|
|
186
|
+
Download the latest files from your snippet into the current directory.
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
cs pull
|
|
190
|
+
cs pull <snippetId>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Existing files will be **overwritten**. New files from the snippet will be created. Files not present in the snippet are left untouched.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### `clone`
|
|
198
|
+
|
|
199
|
+
Clone any **public** snippet into the current directory.
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
cs clone <snippetId>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
A `.csproject` file is written so you can `cs push` changes back (requires ownership).
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
### `log`
|
|
210
|
+
|
|
211
|
+
Show the version history of the current snippet.
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
cs log
|
|
215
|
+
cs log <snippetId>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
v3 — Jan 15, 2026, 3:42 PM
|
|
220
|
+
refactor: split into modules
|
|
221
|
+
|
|
222
|
+
v2 — Jan 14, 2026, 11:20 AM
|
|
223
|
+
add error handling
|
|
224
|
+
|
|
225
|
+
v1 — Jan 13, 2026, 9:05 AM
|
|
226
|
+
initial commit
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
### `diff`
|
|
232
|
+
|
|
233
|
+
Show line-level changes between two versions.
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
cs diff <v1> <v2>
|
|
237
|
+
cs diff 1 3
|
|
238
|
+
cs diff 2 3 <snippetId>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
MODIFIED src/index.ts
|
|
243
|
+
+ export function handleError(e: Error) {
|
|
244
|
+
+ console.error(e.message);
|
|
245
|
+
+ }
|
|
246
|
+
- // TODO: add error handler
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
### `status`
|
|
252
|
+
|
|
253
|
+
Compare your current working files against the latest pushed version.
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
cs status
|
|
257
|
+
cs status <snippetId>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
✓ Clean — up to date with version 3
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
or if there are local changes:
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
Changes since version 3:
|
|
268
|
+
M src/index.ts
|
|
269
|
+
A src/utils.ts
|
|
270
|
+
D src/old.ts
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## File Filtering
|
|
276
|
+
|
|
277
|
+
The CLI automatically ignores certain files and directories. You can add your own exclusions using a `.csignore` file (same syntax as `.gitignore`):
|
|
278
|
+
|
|
279
|
+
```gitignore
|
|
280
|
+
# .csignore
|
|
281
|
+
secrets/
|
|
282
|
+
coverage/
|
|
283
|
+
*.key
|
|
284
|
+
*.pem
|
|
285
|
+
data/*.csv
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Always ignored by default:**
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
.git .svn .hg
|
|
292
|
+
node_modules
|
|
293
|
+
dist build .next out .nuxt
|
|
294
|
+
.cache .parcel-cache .turbo
|
|
295
|
+
*.log
|
|
296
|
+
.DS_Store Thumbs.db
|
|
297
|
+
.env .env.* (exceptions: .env.example)
|
|
298
|
+
*.lock yarn.lock package-lock.json pnpm-lock.yaml
|
|
299
|
+
.csproject
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Your `.gitignore` is also respected automatically.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## .csproject File
|
|
307
|
+
|
|
308
|
+
When you run `cs init` or `cs clone`, a `.csproject` file is created in your directory:
|
|
309
|
+
|
|
310
|
+
```json
|
|
311
|
+
{
|
|
312
|
+
"id": "a1b2c3d4-...",
|
|
313
|
+
"title": "My Project"
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
This file links your local directory to a CodeShare snippet. It can safely be committed to Git.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Limits
|
|
322
|
+
|
|
323
|
+
| Limit | Value |
|
|
324
|
+
|---|---|
|
|
325
|
+
| Max files per push | 500 |
|
|
326
|
+
| Max file size | 512 KB |
|
|
327
|
+
| Max total push size | 50 MB |
|
|
328
|
+
| Max active tokens | 10 per account |
|
|
329
|
+
| Token max length | 200 characters |
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Supported Languages
|
|
334
|
+
|
|
335
|
+
Auto-detected from file extension:
|
|
336
|
+
|
|
337
|
+
`TypeScript` · `JavaScript` · `Python` · `Ruby` · `Go` · `Rust` · `Java` · `C#` · `C/C++` · `HTML` · `CSS` · `SCSS` · `JSON` · `YAML` · `TOML` · `XML` · `Markdown` · `Bash` · `SQL` · `PHP` · `Swift` · `Kotlin` · `R` · `Lua` · `Dart` · `Elixir`
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Requirements
|
|
342
|
+
|
|
343
|
+
- **Node.js** 18 or later
|
|
344
|
+
- A [codeshare.me](https://codeshare.me) account
|
|
345
|
+
- A Personal Access Token (from Settings → Personal Access Tokens)
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## License
|
|
350
|
+
|
|
351
|
+
MIT © [codeshare.me](https://codeshare.me)
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare class APIError extends Error {
|
|
2
|
+
status: number;
|
|
3
|
+
constructor(status: number, message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare const api: {
|
|
6
|
+
post: <T = any>(path: string, body: any, token?: string) => Promise<T>;
|
|
7
|
+
get: <T = any>(path: string, token?: string) => Promise<T>;
|
|
8
|
+
};
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readConfig } from './config.js';
|
|
2
|
+
export class APIError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
constructor(status, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
async function request(method, path, body, token) {
|
|
10
|
+
const cfg = readConfig();
|
|
11
|
+
const host = cfg.host.replace(/\/$/, '');
|
|
12
|
+
const tok = token ?? cfg.token;
|
|
13
|
+
const res = await fetch(`${host}/api${path}`, {
|
|
14
|
+
method,
|
|
15
|
+
headers: {
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
...(tok ? { Authorization: `Bearer ${tok}` } : {}),
|
|
18
|
+
},
|
|
19
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
20
|
+
});
|
|
21
|
+
const json = await res.json().catch(() => ({ error: res.statusText }));
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
throw new APIError(res.status, json?.error ?? json?.message ?? res.statusText);
|
|
24
|
+
}
|
|
25
|
+
return json;
|
|
26
|
+
}
|
|
27
|
+
export const api = {
|
|
28
|
+
post: (path, body, token) => request('POST', path, body, token),
|
|
29
|
+
get: (path, token) => request('GET', path, undefined, token),
|
|
30
|
+
};
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'fs';
|
|
4
|
+
const CONFIG_DIR = join(homedir(), '.codeshare');
|
|
5
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
6
|
+
const defaults = {
|
|
7
|
+
host: 'https://codeshare.me',
|
|
8
|
+
};
|
|
9
|
+
export function readConfig() {
|
|
10
|
+
if (!existsSync(CONFIG_FILE))
|
|
11
|
+
return { ...defaults };
|
|
12
|
+
try {
|
|
13
|
+
return { ...defaults, ...JSON.parse(readFileSync(CONFIG_FILE, 'utf8')) };
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return { ...defaults };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function writeConfig(config) {
|
|
20
|
+
if (!existsSync(CONFIG_DIR))
|
|
21
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
22
|
+
const existing = readConfig();
|
|
23
|
+
writeFileSync(CONFIG_FILE, JSON.stringify({ ...existing, ...config }, null, 2), { mode: 0o600 });
|
|
24
|
+
// Ensure permissions are tight even if files/dirs already existed
|
|
25
|
+
try {
|
|
26
|
+
chmodSync(CONFIG_DIR, 0o700);
|
|
27
|
+
}
|
|
28
|
+
catch { /* non-fatal */ }
|
|
29
|
+
try {
|
|
30
|
+
chmodSync(CONFIG_FILE, 0o600);
|
|
31
|
+
}
|
|
32
|
+
catch { /* non-fatal */ }
|
|
33
|
+
}
|
|
34
|
+
export function requireToken() {
|
|
35
|
+
const { token } = readConfig();
|
|
36
|
+
if (!token) {
|
|
37
|
+
console.error('Not logged in. Run: cs login --token <your-token>');
|
|
38
|
+
console.error('Get a token at: https://codeshare.me/settings (Personal Access Tokens section)');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
return token;
|
|
42
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { readConfig, writeConfig } from './config.js';
|
|
5
|
+
import { writeProject, requireProject } from './project.js';
|
|
6
|
+
import { scanFiles } from './scanner.js';
|
|
7
|
+
import { api } from './api.js';
|
|
8
|
+
import { requireToken } from './config.js';
|
|
9
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
10
|
+
import { join, dirname } from 'path';
|
|
11
|
+
import ora from 'ora';
|
|
12
|
+
import prompts from 'prompts';
|
|
13
|
+
const program = new Command();
|
|
14
|
+
program
|
|
15
|
+
.name('cs')
|
|
16
|
+
.description('CodeShare CLI — version-control your code snippets')
|
|
17
|
+
.version('1.0.0');
|
|
18
|
+
// ─── LOGIN ────────────────────────────────────────────────────────────────────
|
|
19
|
+
program
|
|
20
|
+
.command('login')
|
|
21
|
+
.description('Save your personal access token')
|
|
22
|
+
.option('--token <token>', 'Personal access token from codeshare.me/settings')
|
|
23
|
+
.option('--host <host>', 'Custom host (default: https://codeshare.me)')
|
|
24
|
+
.action(async (opts) => {
|
|
25
|
+
let token = opts.token;
|
|
26
|
+
let host = opts.host;
|
|
27
|
+
if (!token) {
|
|
28
|
+
const res = await prompts({ type: 'password', name: 'token', message: 'Token (from codeshare.me/settings):' });
|
|
29
|
+
token = res.token;
|
|
30
|
+
}
|
|
31
|
+
if (!token) {
|
|
32
|
+
console.error(chalk.red('No token provided'));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
if (host)
|
|
36
|
+
writeConfig({ host });
|
|
37
|
+
// Verify token by hitting a simple endpoint
|
|
38
|
+
const spinner = ora('Verifying token…').start();
|
|
39
|
+
try {
|
|
40
|
+
const data = await api.get('/cli/me', token);
|
|
41
|
+
spinner.succeed(chalk.green(`Logged in as ${data.user?.username ?? 'user'}!`));
|
|
42
|
+
writeConfig({ token });
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
spinner.fail(chalk.red(`Token verification failed: ${e.message}`));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
// ─── LOGOUT ───────────────────────────────────────────────────────────────────
|
|
50
|
+
program
|
|
51
|
+
.command('logout')
|
|
52
|
+
.description('Remove saved token')
|
|
53
|
+
.action(() => {
|
|
54
|
+
writeConfig({ token: undefined });
|
|
55
|
+
console.log(chalk.green('Logged out.'));
|
|
56
|
+
});
|
|
57
|
+
// ─── INIT ─────────────────────────────────────────────────────────────────────
|
|
58
|
+
program
|
|
59
|
+
.command('init')
|
|
60
|
+
.description('Create a new snippet and link this directory to it')
|
|
61
|
+
.option('--title <title>', 'Snippet title')
|
|
62
|
+
.option('--id <id>', 'Link to an existing snippet ID instead of creating a new one')
|
|
63
|
+
.option('--private', 'Make the snippet private')
|
|
64
|
+
.action(async (opts) => {
|
|
65
|
+
const token = requireToken();
|
|
66
|
+
if (opts.id) {
|
|
67
|
+
writeProject({ id: opts.id });
|
|
68
|
+
console.log(chalk.green(`✓ Linked to snippet ${opts.id}`));
|
|
69
|
+
console.log('Run ' + chalk.cyan('cs push') + ' to upload your files.');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let title = opts.title;
|
|
73
|
+
if (!title) {
|
|
74
|
+
const res = await prompts({ type: 'text', name: 'title', message: 'Snippet title:', initial: dirname(process.cwd()).split('/').pop() ?? 'My Project' });
|
|
75
|
+
title = res.title;
|
|
76
|
+
}
|
|
77
|
+
if (!title) {
|
|
78
|
+
console.error(chalk.red('Title is required'));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const spinner = ora('Creating snippet…').start();
|
|
82
|
+
try {
|
|
83
|
+
const data = await api.post('/cli/init', { title, isPublic: !opts.private, language: 'text' }, token);
|
|
84
|
+
spinner.succeed(chalk.green(`Snippet created: ${data.snippet.url}`));
|
|
85
|
+
writeProject({ id: data.snippet.id, title });
|
|
86
|
+
console.log(chalk.gray('.csproject written — run ') + chalk.cyan('cs push') + chalk.gray(' to upload your files.'));
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
spinner.fail(chalk.red(e.message));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// ─── PUSH ─────────────────────────────────────────────────────────────────────
|
|
94
|
+
program
|
|
95
|
+
.command('push [snippetId]')
|
|
96
|
+
.description('Push local files to CodeShare (creates a new version)')
|
|
97
|
+
.option('-m, --message <message>', 'Commit message', 'Update')
|
|
98
|
+
.option('--dir <dir>', 'Directory to push (default: current directory)', process.cwd())
|
|
99
|
+
.action(async (snippetId, opts) => {
|
|
100
|
+
const token = requireToken();
|
|
101
|
+
const project = snippetId ? { id: snippetId } : requireProject();
|
|
102
|
+
const spinner = ora('Scanning files…').start();
|
|
103
|
+
const files = scanFiles(opts.dir);
|
|
104
|
+
if (files.length === 0) {
|
|
105
|
+
spinner.fail(chalk.red('No files found to push.'));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
spinner.text = `Pushing ${files.length} files…`;
|
|
109
|
+
try {
|
|
110
|
+
const data = await api.post(`/cli/push/${project.id}`, { message: opts.message, files }, token);
|
|
111
|
+
if (!snippetId)
|
|
112
|
+
writeProject({ ...project }); // ensure .csproject stays
|
|
113
|
+
const host = readConfig().host;
|
|
114
|
+
spinner.succeed(chalk.green(`✓ Pushed ${data.filesUploaded} files → version ${data.version}`));
|
|
115
|
+
console.log(chalk.gray(' URL: ') + chalk.cyan(`${host}/c/${project.id}`));
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
spinner.fail(chalk.red(e.message));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
// ─── PULL ─────────────────────────────────────────────────────────────────────
|
|
123
|
+
program
|
|
124
|
+
.command('pull [snippetId]')
|
|
125
|
+
.description('Pull latest files from CodeShare into current directory')
|
|
126
|
+
.option('--dir <dir>', 'Target directory (default: current directory)', process.cwd())
|
|
127
|
+
.action(async (snippetId, opts) => {
|
|
128
|
+
const token = requireToken();
|
|
129
|
+
const project = snippetId ? { id: snippetId } : requireProject();
|
|
130
|
+
const spinner = ora('Fetching files…').start();
|
|
131
|
+
try {
|
|
132
|
+
const data = await api.get(`/cli/pull/${project.id}`, token);
|
|
133
|
+
spinner.text = `Writing ${data.files.length} files…`;
|
|
134
|
+
for (const f of data.files) {
|
|
135
|
+
if (f.type !== 'file')
|
|
136
|
+
continue;
|
|
137
|
+
const dest = join(opts.dir, f.path);
|
|
138
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
139
|
+
writeFileSync(dest, f.content ?? '', 'utf8');
|
|
140
|
+
}
|
|
141
|
+
writeProject({ id: project.id, title: data.snippet?.title });
|
|
142
|
+
spinner.succeed(chalk.green(`✓ Pulled ${data.files.filter((f) => f.type === 'file').length} files (version ${data.latestVersion})`));
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
spinner.fail(chalk.red(e.message));
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
// ─── CLONE ────────────────────────────────────────────────────────────────────
|
|
150
|
+
program
|
|
151
|
+
.command('clone <snippetId> [dir]')
|
|
152
|
+
.description('Clone a public snippet into a new directory')
|
|
153
|
+
.action(async (snippetId, dir) => {
|
|
154
|
+
const token = requireToken();
|
|
155
|
+
const targetDir = dir ?? snippetId;
|
|
156
|
+
const spinner = ora(`Cloning ${snippetId}…`).start();
|
|
157
|
+
try {
|
|
158
|
+
const data = await api.get(`/cli/clone/${snippetId}`, token);
|
|
159
|
+
mkdirSync(targetDir, { recursive: true });
|
|
160
|
+
for (const f of data.files) {
|
|
161
|
+
if (f.type !== 'file')
|
|
162
|
+
continue;
|
|
163
|
+
const dest = join(targetDir, f.path);
|
|
164
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
165
|
+
writeFileSync(dest, f.content ?? '', 'utf8');
|
|
166
|
+
}
|
|
167
|
+
writeProject({ id: snippetId, title: data.snippet?.title }, targetDir);
|
|
168
|
+
spinner.succeed(chalk.green(`✓ Cloned '${data.snippet.title}' into ./${targetDir}/ (${data.files.length} files)`));
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
spinner.fail(chalk.red(e.message));
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
// ─── LOG ──────────────────────────────────────────────────────────────────────
|
|
176
|
+
program
|
|
177
|
+
.command('log [snippetId]')
|
|
178
|
+
.description('Show version history')
|
|
179
|
+
.action(async (snippetId) => {
|
|
180
|
+
const token = requireToken();
|
|
181
|
+
const project = snippetId ? { id: snippetId } : requireProject();
|
|
182
|
+
try {
|
|
183
|
+
const data = await api.get(`/cli/log/${project.id}`, token);
|
|
184
|
+
if (data.versions.length === 0) {
|
|
185
|
+
console.log(chalk.gray('No versions yet.'));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
for (const v of data.versions) {
|
|
189
|
+
const date = new Date(v.createdAt).toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
190
|
+
console.log(chalk.yellow(`v${v.versionNumber}`) + chalk.gray(` — ${date}`));
|
|
191
|
+
console.log(` ${v.changeLog ?? '(no message)'}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
console.error(chalk.red(e.message));
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
// ─── STATUS ───────────────────────────────────────────────────────────────────
|
|
200
|
+
program
|
|
201
|
+
.command('status [snippetId]')
|
|
202
|
+
.description('Show what has changed vs the latest version')
|
|
203
|
+
.action(async (snippetId) => {
|
|
204
|
+
const token = requireToken();
|
|
205
|
+
const project = snippetId ? { id: snippetId } : requireProject();
|
|
206
|
+
try {
|
|
207
|
+
const data = await api.get(`/cli/status/${project.id}`, token);
|
|
208
|
+
if (data.status === 'unversioned') {
|
|
209
|
+
console.log(chalk.gray('No versions yet — push to create the first one.'));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (data.clean) {
|
|
213
|
+
console.log(chalk.green(`✓ Clean — up to date with version ${data.latestVersion}`));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
console.log(chalk.yellow(`Changes since version ${data.latestVersion}:`));
|
|
217
|
+
for (const c of data.changes) {
|
|
218
|
+
const symbol = c.status === 'added' ? chalk.green('+') : c.status === 'removed' ? chalk.red('-') : chalk.yellow('M');
|
|
219
|
+
console.log(` ${symbol} ${c.path}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
console.error(chalk.red(e.message));
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
// ─── DIFF ─────────────────────────────────────────────────────────────────────
|
|
228
|
+
program
|
|
229
|
+
.command('diff <v1> <v2> [snippetId]')
|
|
230
|
+
.description('Show diff between two versions')
|
|
231
|
+
.action(async (v1, v2, snippetId) => {
|
|
232
|
+
const token = requireToken();
|
|
233
|
+
const project = snippetId ? { id: snippetId } : requireProject();
|
|
234
|
+
try {
|
|
235
|
+
const data = await api.get(`/cli/diff/${project.id}/${v1}/${v2}`, token);
|
|
236
|
+
for (const file of data.diff) {
|
|
237
|
+
if (file.status === 'unchanged')
|
|
238
|
+
continue;
|
|
239
|
+
const color = file.status === 'added' ? chalk.greenBright : file.status === 'removed' ? chalk.redBright : chalk.blueBright;
|
|
240
|
+
console.log(color(`\n${file.status.toUpperCase()} ${file.path}`));
|
|
241
|
+
if (file.lines) {
|
|
242
|
+
for (const line of file.lines) {
|
|
243
|
+
if (line.type === '+')
|
|
244
|
+
console.log(chalk.green(`+ ${line.text}`));
|
|
245
|
+
else if (line.type === '-')
|
|
246
|
+
console.log(chalk.red(`- ${line.text}`));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
console.error(chalk.red(e.message));
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
// ─── WHOAMI ───────────────────────────────────────────────────────────────────
|
|
257
|
+
program
|
|
258
|
+
.command('whoami')
|
|
259
|
+
.description('Show current login info')
|
|
260
|
+
.action(async () => {
|
|
261
|
+
const cfg = readConfig();
|
|
262
|
+
if (!cfg.token) {
|
|
263
|
+
console.log(chalk.gray('Not logged in. Run: cs login --token <token>'));
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
try {
|
|
267
|
+
const data = await api.get('/cli/me', cfg.token);
|
|
268
|
+
console.log(chalk.green(`Logged in as ${data.user?.username ?? 'unknown'}`));
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
console.log(chalk.green('Logged in'));
|
|
272
|
+
}
|
|
273
|
+
console.log(chalk.gray(` Host: ${cfg.host}`));
|
|
274
|
+
console.log(chalk.gray(` Token: ${cfg.token.slice(0, 10)}…`));
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
program.parse();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ProjectConfig {
|
|
2
|
+
id: string;
|
|
3
|
+
title?: string;
|
|
4
|
+
host?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function readProject(cwd?: string): ProjectConfig | null;
|
|
7
|
+
export declare function writeProject(config: ProjectConfig, cwd?: string): void;
|
|
8
|
+
export declare function requireProject(cwd?: string): ProjectConfig;
|
package/dist/project.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .csproject — local per-repo config file (committed to snippet)
|
|
3
|
+
* Stores snippetId so cs push/pull/etc know which snippet this maps to
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
const PROJECT_FILE = '.csproject';
|
|
8
|
+
export function readProject(cwd = process.cwd()) {
|
|
9
|
+
const file = join(cwd, PROJECT_FILE);
|
|
10
|
+
if (!existsSync(file))
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function writeProject(config, cwd = process.cwd()) {
|
|
20
|
+
writeFileSync(join(cwd, PROJECT_FILE), JSON.stringify(config, null, 2));
|
|
21
|
+
}
|
|
22
|
+
export function requireProject(cwd = process.cwd()) {
|
|
23
|
+
const p = readProject(cwd);
|
|
24
|
+
if (!p) {
|
|
25
|
+
console.error('No .csproject file found. Run "cs init" or "cs clone <id>" first.');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
return p;
|
|
29
|
+
}
|
package/dist/scanner.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync, existsSync } from 'fs';
|
|
2
|
+
import { join, relative } from 'path';
|
|
3
|
+
import ignore from 'ignore';
|
|
4
|
+
const DEFAULT_IGNORE = [
|
|
5
|
+
'.git', '.svn', '.hg',
|
|
6
|
+
'node_modules', '.pnp', '.pnp.js',
|
|
7
|
+
'dist', 'build', '.next', 'out', '.nuxt',
|
|
8
|
+
'.cache', '.parcel-cache', '.turbo',
|
|
9
|
+
'*.log', 'npm-debug.log*', 'yarn-debug.log*',
|
|
10
|
+
'.DS_Store', 'Thumbs.db', 'desktop.ini',
|
|
11
|
+
'.env', '.env.*', '!.env.example',
|
|
12
|
+
'.csproject',
|
|
13
|
+
'*.lock', 'yarn.lock', 'package-lock.json', 'pnpm-lock.yaml',
|
|
14
|
+
];
|
|
15
|
+
const MAX_FILE_SIZE = 512 * 1024; // 512 KB per file
|
|
16
|
+
const MAX_TOTAL_FILES = 500;
|
|
17
|
+
function guessLanguage(filename) {
|
|
18
|
+
const ext = filename.split('.').pop()?.toLowerCase() ?? '';
|
|
19
|
+
const map = {
|
|
20
|
+
ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript',
|
|
21
|
+
py: 'python', rb: 'ruby', go: 'go', rs: 'rust', java: 'java',
|
|
22
|
+
cs: 'csharp', cpp: 'cpp', c: 'c', h: 'c', hpp: 'cpp',
|
|
23
|
+
html: 'html', css: 'css', scss: 'scss', sass: 'sass',
|
|
24
|
+
json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml', xml: 'xml',
|
|
25
|
+
md: 'markdown', mdx: 'markdown', sh: 'bash', bash: 'bash',
|
|
26
|
+
sql: 'sql', php: 'php', swift: 'swift', kt: 'kotlin',
|
|
27
|
+
r: 'r', lua: 'lua', dart: 'dart', ex: 'elixir', exs: 'elixir',
|
|
28
|
+
};
|
|
29
|
+
return map[ext] ?? 'text';
|
|
30
|
+
}
|
|
31
|
+
export function scanFiles(cwd) {
|
|
32
|
+
const ig = ignore().add(DEFAULT_IGNORE);
|
|
33
|
+
const csignorePath = join(cwd, '.csignore');
|
|
34
|
+
if (existsSync(csignorePath)) {
|
|
35
|
+
ig.add(readFileSync(csignorePath, 'utf8'));
|
|
36
|
+
}
|
|
37
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
38
|
+
if (existsSync(gitignorePath)) {
|
|
39
|
+
ig.add(readFileSync(gitignorePath, 'utf8'));
|
|
40
|
+
}
|
|
41
|
+
const files = [];
|
|
42
|
+
let mainFile = null;
|
|
43
|
+
function walk(dir) {
|
|
44
|
+
if (files.length >= MAX_TOTAL_FILES)
|
|
45
|
+
return;
|
|
46
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
47
|
+
for (const e of entries) {
|
|
48
|
+
const fullPath = join(dir, e.name);
|
|
49
|
+
const relPath = relative(cwd, fullPath).replace(/\\/g, '/');
|
|
50
|
+
if (ig.ignores(relPath))
|
|
51
|
+
continue;
|
|
52
|
+
if (e.isDirectory()) {
|
|
53
|
+
walk(fullPath);
|
|
54
|
+
}
|
|
55
|
+
else if (e.isFile()) {
|
|
56
|
+
const stat = statSync(fullPath);
|
|
57
|
+
if (stat.size > MAX_FILE_SIZE)
|
|
58
|
+
continue;
|
|
59
|
+
try {
|
|
60
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
61
|
+
const lang = guessLanguage(e.name);
|
|
62
|
+
// Detect main file: index.*, main.*, App.*
|
|
63
|
+
const isMain = /^(index|main|app)\.(ts|tsx|js|jsx|py|go|rs|java|rb)$/i.test(e.name);
|
|
64
|
+
if (isMain && !mainFile)
|
|
65
|
+
mainFile = relPath;
|
|
66
|
+
files.push({ name: e.name, path: relPath, content, language: lang, type: 'file', isMain: false });
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Skip binary files or unreadable files
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
walk(cwd);
|
|
75
|
+
// Mark the main file
|
|
76
|
+
if (mainFile) {
|
|
77
|
+
const mf = files.find(f => f.path === mainFile);
|
|
78
|
+
if (mf)
|
|
79
|
+
mf.isMain = true;
|
|
80
|
+
}
|
|
81
|
+
return files;
|
|
82
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codeshareme/codeshare-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official CLI for codeshare.me — push, pull and version-control your code snippets",
|
|
5
|
+
"homepage": "https://codeshare.me",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"codeshare",
|
|
9
|
+
"cli",
|
|
10
|
+
"code",
|
|
11
|
+
"snippets",
|
|
12
|
+
"version-control"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"cs": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"dev": "tsx src/index.ts",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"commander": "^12.0.0",
|
|
31
|
+
"chalk": "^5.3.0",
|
|
32
|
+
"ora": "^8.0.1",
|
|
33
|
+
"glob": "^10.4.1",
|
|
34
|
+
"ignore": "^5.3.1",
|
|
35
|
+
"diff": "^5.2.0",
|
|
36
|
+
"prompts": "^2.4.2",
|
|
37
|
+
"fs-extra": "^11.2.0",
|
|
38
|
+
"node-fetch": "^3.3.2"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^20.0.0",
|
|
42
|
+
"@types/diff": "^5.2.1",
|
|
43
|
+
"@types/fs-extra": "^11.0.4",
|
|
44
|
+
"@types/prompts": "^2.4.9",
|
|
45
|
+
"typescript": "^5.4.0",
|
|
46
|
+
"tsx": "^4.7.0"
|
|
47
|
+
},
|
|
48
|
+
"type": "module"
|
|
49
|
+
}
|