@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 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
+ };
@@ -0,0 +1,7 @@
1
+ export interface Config {
2
+ token?: string;
3
+ host: string;
4
+ }
5
+ export declare function readConfig(): Config;
6
+ export declare function writeConfig(config: Partial<Config>): void;
7
+ export declare function requireToken(): string;
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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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;
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ export interface LocalFile {
2
+ name: string;
3
+ path: string;
4
+ content: string;
5
+ language: string;
6
+ type: 'file';
7
+ isMain: boolean;
8
+ }
9
+ export declare function scanFiles(cwd: string): LocalFile[];
@@ -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
+ }