thinkncollab-cli 0.0.9 β 0.0.11
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 +305 -3
- package/bin/index.js +101 -14
- package/branch/.tnc/.tncmeta.json +7 -0
- package/branch/init.js +47 -0
- package/branch/pull.js +124 -0
- package/package.json +1 -1
package/Readme.md
CHANGED
|
@@ -1,11 +1,313 @@
|
|
|
1
|
-
|
|
1
|
+
# π§ ThinkNCollab CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A powerful command-line interface for seamless collaboration with **ThinkNCollab** β push files, manage rooms, and collaborate directly from your terminal.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
6
|
|
|
7
|
+
## π Quick Start
|
|
7
8
|
|
|
9
|
+
```bash
|
|
10
|
+
# Install the CLI globally
|
|
11
|
+
npm install -g @thinkncollab/tnc-cli
|
|
8
12
|
|
|
13
|
+
# Login to your ThinkNCollab account
|
|
14
|
+
tnc-cli login
|
|
9
15
|
|
|
16
|
+
# Push files to a room
|
|
17
|
+
tnc-cli push --room <roomId> <path>
|
|
10
18
|
|
|
19
|
+
# Logout
|
|
20
|
+
tnc-cli logout
|
|
21
|
+
```
|
|
11
22
|
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## π Authentication
|
|
26
|
+
|
|
27
|
+
### **Login Command**
|
|
28
|
+
|
|
29
|
+
Authenticate with your ThinkNCollab account to enable CLI access:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
tnc-cli login
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### **What Happens During Login**
|
|
36
|
+
|
|
37
|
+
- Opens a secure browser window to ThinkNCollabβs authentication page
|
|
38
|
+
- Completes OAuth2 authentication flow
|
|
39
|
+
- Creates an encrypted `.tncrc` configuration file in your home directory
|
|
40
|
+
- Stores secure tokens for future CLI sessions
|
|
41
|
+
|
|
42
|
+
### **Manual Authentication**
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
tnc-cli login --token YOUR_AUTH_TOKEN
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### **Verify Authentication**
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
tnc-cli whoami
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### **Logout**
|
|
55
|
+
|
|
56
|
+
Clear stored credentials:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
tnc-cli logout
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## π¦ File Operations
|
|
65
|
+
|
|
66
|
+
### **Push Command**
|
|
67
|
+
|
|
68
|
+
Push files or directories to ThinkNCollab rooms:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
tnc-cli push --room <roomId> <path>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### **Syntax**
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
tnc-cli push --room ROOM_ID PATH [ADDITIONAL_PATHS...]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### **Examples**
|
|
81
|
+
|
|
82
|
+
| Action | Command |
|
|
83
|
+
|--------|----------|
|
|
84
|
+
| Push a single file | `tnc-cli push --room 64a1b2c3d4e5f6a1b2c3d4e5 document.pdf` |
|
|
85
|
+
| Push entire folder | `tnc-cli push --room 64a1b2c3d4e5f6a1b2c3d4e5 ./src/` |
|
|
86
|
+
| Push multiple items | `tnc-cli push --room 64a1b2c3d4e5f6a1b2c3d4e5 file1.js assets/ components/` |
|
|
87
|
+
| Push current directory | `tnc-cli push --room 64a1b2c3d4e5f6a1b2c3d4e5 .` |
|
|
88
|
+
|
|
89
|
+
#### **Options**
|
|
90
|
+
|
|
91
|
+
| Option | Short | Description |
|
|
92
|
+
|---------|--------|-------------|
|
|
93
|
+
| `--room` | `-r` | **Required:** Target room ID |
|
|
94
|
+
| `--message` | `-m` | Commit message describing changes |
|
|
95
|
+
| `--force` | `-f` | Force push (overwrite conflicts) |
|
|
96
|
+
| `--dry-run` | β | Preview files before pushing |
|
|
97
|
+
| `--exclude` | β | Additional patterns to exclude |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## π§© Room Management
|
|
102
|
+
|
|
103
|
+
| Command | Description |
|
|
104
|
+
|----------|--------------|
|
|
105
|
+
| `tnc-cli rooms list` | List accessible rooms |
|
|
106
|
+
| `tnc-cli rooms info <id>` | Show details for a specific room |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## π« File Ignoring
|
|
111
|
+
|
|
112
|
+
Use a `.ignoretnc` file in your project root to exclude files/folders during push.
|
|
113
|
+
|
|
114
|
+
### **Example `.ignoretnc`**
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
# Dependencies
|
|
118
|
+
node_modules/
|
|
119
|
+
vendor/
|
|
120
|
+
bower_components/
|
|
121
|
+
|
|
122
|
+
# Build outputs
|
|
123
|
+
/dist
|
|
124
|
+
/build
|
|
125
|
+
/.next
|
|
126
|
+
/out
|
|
127
|
+
|
|
128
|
+
# Environment
|
|
129
|
+
.env
|
|
130
|
+
.env.local
|
|
131
|
+
.env.production
|
|
132
|
+
.env.development
|
|
133
|
+
|
|
134
|
+
# Logs
|
|
135
|
+
*.log
|
|
136
|
+
npm-debug.log*
|
|
137
|
+
yarn-debug.log*
|
|
138
|
+
|
|
139
|
+
# Temporary / OS
|
|
140
|
+
*.tmp
|
|
141
|
+
.DS_Store
|
|
142
|
+
Thumbs.db
|
|
143
|
+
|
|
144
|
+
# IDE
|
|
145
|
+
.vscode/
|
|
146
|
+
.idea/
|
|
147
|
+
|
|
148
|
+
# Test
|
|
149
|
+
*.test.js
|
|
150
|
+
*.spec.js
|
|
151
|
+
/coverage/
|
|
152
|
+
|
|
153
|
+
# Large assets
|
|
154
|
+
*.psd
|
|
155
|
+
*.ai
|
|
156
|
+
*.sketch
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### **Pattern Rules**
|
|
160
|
+
|
|
161
|
+
| Type | Example | Description |
|
|
162
|
+
|------|----------|-------------|
|
|
163
|
+
| Directory | `dist/` | Ignore whole directory |
|
|
164
|
+
| File Extension | `*.log` | Ignore all `.log` files |
|
|
165
|
+
| Specific File | `secret.env` | Ignore single file |
|
|
166
|
+
| Wildcard | `test-*.js` | Match name patterns |
|
|
167
|
+
| Negation | `!keep.js` | Include despite other rules |
|
|
168
|
+
| Comment | `# comment` | Ignored by parser |
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## βοΈ Configuration
|
|
173
|
+
|
|
174
|
+
After login, an encrypted `.tncrc` file is created in your home directory.
|
|
175
|
+
|
|
176
|
+
### **Example `.tncrc`**
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"user": {
|
|
181
|
+
"id": "encrypted_user_id",
|
|
182
|
+
"email": "encrypted_email",
|
|
183
|
+
"name": "encrypted_display_name"
|
|
184
|
+
},
|
|
185
|
+
"auth": {
|
|
186
|
+
"token": "encrypted_jwt_token",
|
|
187
|
+
"refreshToken": "encrypted_refresh_token",
|
|
188
|
+
"expires": "2025-12-31T23:59:59Z"
|
|
189
|
+
},
|
|
190
|
+
"workspace": {
|
|
191
|
+
"id": "encrypted_workspace_id",
|
|
192
|
+
"name": "encrypted_workspace_name"
|
|
193
|
+
},
|
|
194
|
+
"settings": {
|
|
195
|
+
"defaultRoom": "optional_default_room_id",
|
|
196
|
+
"autoSync": false
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### **Environment Variables**
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
export TNC_API_TOKEN="your_api_token"
|
|
205
|
+
export TNC_API_URL="https://api.thinkncollab.com"
|
|
206
|
+
export TNC_DEFAULT_ROOM="your_default_room_id"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## β‘ Advanced Usage
|
|
212
|
+
|
|
213
|
+
### **Batch Push**
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
tnc-cli push --room room1,room2,room3 ./shared-assets/
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### **CI/CD Integration**
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
tnc-cli login --token $TNC_DEPLOY_TOKEN
|
|
223
|
+
tnc-cli push --room $PRODUCTION_ROOM ./dist/ --message "Build ${CI_COMMIT_SHA}"
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### **Watch for Changes (Experimental)**
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
tnc-cli watch --room 64a1b2c3d4e5f6a1b2c3d4e5 ./src/
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## π§° Troubleshooting
|
|
235
|
+
|
|
236
|
+
### **Authentication Issues**
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
tnc-cli logout
|
|
240
|
+
tnc-cli login
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
- Ensure valid token and room access
|
|
244
|
+
- Token may need refresh or rotation
|
|
245
|
+
|
|
246
|
+
### **Permission Errors**
|
|
247
|
+
|
|
248
|
+
- Confirm write access to target room
|
|
249
|
+
- Check if the room ID is active
|
|
250
|
+
|
|
251
|
+
### **File Size Limits**
|
|
252
|
+
|
|
253
|
+
| Type | Limit |
|
|
254
|
+
|------|--------|
|
|
255
|
+
| Individual File | 100 MB |
|
|
256
|
+
| Total Push | 1 GB |
|
|
257
|
+
|
|
258
|
+
### **Debug Mode**
|
|
259
|
+
|
|
260
|
+
Enable detailed logs:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
tnc-cli --debug push --room 64a1b2c3d4e5f6a1b2c3d4e5 ./path/
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## π Security Guidelines
|
|
269
|
+
|
|
270
|
+
- **Never share** your `.tncrc` file β it stores encrypted tokens
|
|
271
|
+
- **Never commit** `.tncrc` to Git or any version control
|
|
272
|
+
- Use `.ignoretnc` to exclude sensitive files
|
|
273
|
+
- Rotate API tokens regularly
|
|
274
|
+
- Validate room access before pushing confidential data
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## π‘ Best Practices
|
|
279
|
+
|
|
280
|
+
- Use environment variables for automated environments
|
|
281
|
+
- Review `.ignoretnc` before each push
|
|
282
|
+
- Run `--dry-run` to preview changes
|
|
283
|
+
- Monitor push logs for unexpected files
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## π§ Command Reference
|
|
288
|
+
|
|
289
|
+
| Command | Description |
|
|
290
|
+
|----------|-------------|
|
|
291
|
+
| `tnc-cli login` | Authenticate with ThinkNCollab |
|
|
292
|
+
| `tnc-cli logout` | Clear credentials |
|
|
293
|
+
| `tnc-cli whoami` | Show current user info |
|
|
294
|
+
| `tnc-cli push --room <id> <path>` | Push files/folders to a room |
|
|
295
|
+
| `tnc-cli rooms list` | List all accessible rooms |
|
|
296
|
+
| `tnc-cli rooms info <id>` | Show room details |
|
|
297
|
+
| `tnc-cli --version` | Show CLI version |
|
|
298
|
+
| `tnc-cli --help` | Show help information |
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## π§© Resources & Support
|
|
303
|
+
|
|
304
|
+
- π **Documentation:** [docs.thinkncollab.com/cli](https://docs.thinkncollab.com/cli)
|
|
305
|
+
- π **GitHub Issues:** [ThinkNCollab Repository](https://github.com/thinkncollab)
|
|
306
|
+
- βοΈ **Email:** support@thinkncollab.com
|
|
307
|
+
- π¬ **Community:** Join our [ThinkNCollab Discord](https://discord.gg/thinkncollab)
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## π License
|
|
312
|
+
|
|
313
|
+
MIT License β see `LICENSE` file for details.
|
package/bin/index.js
CHANGED
|
@@ -8,9 +8,11 @@ import crypto from "crypto";
|
|
|
8
8
|
import FormData from "form-data";
|
|
9
9
|
|
|
10
10
|
const RC_FILE = path.join(os.homedir(), ".tncrc");
|
|
11
|
-
const
|
|
11
|
+
const VERSION_FILE = path.join(process.cwd(), ".tncversions");
|
|
12
|
+
const BASE_URL = "http://localhost:3001/rooms";
|
|
13
|
+
const CWD = process.cwd();
|
|
12
14
|
|
|
13
|
-
/**
|
|
15
|
+
/** ------------------ LOGIN ------------------ **/
|
|
14
16
|
async function login() {
|
|
15
17
|
const answers = await inquirer.prompt([
|
|
16
18
|
{ type: "input", name: "email", message: "Email:" },
|
|
@@ -32,6 +34,21 @@ async function login() {
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
/** ------------------ LOGOUT ------------------ **/
|
|
38
|
+
async function logout() {
|
|
39
|
+
try {
|
|
40
|
+
if (fs.existsSync(RC_FILE)) {
|
|
41
|
+
await fs.promises.rm(RC_FILE, { force: true });
|
|
42
|
+
console.log("β
Logged out successfully. Local credentials removed.");
|
|
43
|
+
} else {
|
|
44
|
+
console.log("βΉοΈ No active session found.");
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error("β Error during logout:", err.message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** ------------------ TOKEN UTILS ------------------ **/
|
|
35
52
|
function readToken() {
|
|
36
53
|
if (!fs.existsSync(RC_FILE)) {
|
|
37
54
|
console.error("β Not logged in. Run 'tnc login' first.");
|
|
@@ -41,7 +58,7 @@ function readToken() {
|
|
|
41
58
|
return { token: data.token, email: data.email };
|
|
42
59
|
}
|
|
43
60
|
|
|
44
|
-
/**
|
|
61
|
+
/** ------------------ IGNORE HANDLING ------------------ **/
|
|
45
62
|
function loadIgnore(folderPath) {
|
|
46
63
|
const ignoreFile = path.join(folderPath, ".ignoretnc");
|
|
47
64
|
if (!fs.existsSync(ignoreFile)) return [];
|
|
@@ -65,7 +82,7 @@ function shouldIgnore(relativePath, ignoreList) {
|
|
|
65
82
|
});
|
|
66
83
|
}
|
|
67
84
|
|
|
68
|
-
/**
|
|
85
|
+
/** ------------------ SCAN FOLDER ------------------ **/
|
|
69
86
|
function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
|
|
70
87
|
const items = fs.readdirSync(folderPath, { withFileTypes: true });
|
|
71
88
|
const result = [];
|
|
@@ -82,14 +99,15 @@ function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
|
|
|
82
99
|
result.push({
|
|
83
100
|
name: item.name,
|
|
84
101
|
type: "folder",
|
|
85
|
-
children: scanFolder(fullPath, ignoreList, rootPath)
|
|
102
|
+
children: scanFolder(fullPath, ignoreList, rootPath),
|
|
103
|
+
path: relativePath
|
|
86
104
|
});
|
|
87
105
|
} else {
|
|
88
106
|
const stats = fs.statSync(fullPath);
|
|
89
107
|
result.push({
|
|
90
108
|
name: item.name,
|
|
91
109
|
type: "file",
|
|
92
|
-
path:
|
|
110
|
+
path: relativePath,
|
|
93
111
|
size: stats.size
|
|
94
112
|
});
|
|
95
113
|
}
|
|
@@ -97,7 +115,48 @@ function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
|
|
|
97
115
|
return result;
|
|
98
116
|
}
|
|
99
117
|
|
|
100
|
-
/**
|
|
118
|
+
/** ------------------ VERSIONING ------------------ **/
|
|
119
|
+
function loadVersions() {
|
|
120
|
+
if (!fs.existsSync(VERSION_FILE)) return {};
|
|
121
|
+
return JSON.parse(fs.readFileSync(VERSION_FILE, "utf-8"));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function saveVersions(versionMap) {
|
|
125
|
+
fs.writeFileSync(VERSION_FILE, JSON.stringify(versionMap, null, 2));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function computeHashCLI(filePath) {
|
|
129
|
+
if (!fs.existsSync(filePath)) return null;
|
|
130
|
+
const stats = fs.statSync(filePath);
|
|
131
|
+
if (stats.isDirectory()) {
|
|
132
|
+
const items = fs.readdirSync(filePath);
|
|
133
|
+
const combined = items.map(name => computeHashCLI(path.join(filePath, name))).join("");
|
|
134
|
+
return crypto.createHash("sha256").update(filePath + combined).digest("hex");
|
|
135
|
+
} else {
|
|
136
|
+
const content = fs.readFileSync(filePath);
|
|
137
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function checkChanges(fileTree, versionMap, rootPath = CWD) {
|
|
142
|
+
return fileTree.map(item => {
|
|
143
|
+
const fullPath = path.join(rootPath, item.path || item.name);
|
|
144
|
+
const hash = computeHashCLI(fullPath);
|
|
145
|
+
const prevHash = versionMap[item.path || fullPath] || null;
|
|
146
|
+
const changed = hash !== prevHash;
|
|
147
|
+
|
|
148
|
+
let newItem = { ...item, changed, hash };
|
|
149
|
+
|
|
150
|
+
if (item.type === "folder" && item.children.length > 0) {
|
|
151
|
+
newItem.children = checkChanges(item.children, versionMap, rootPath);
|
|
152
|
+
newItem.changed = newItem.changed || newItem.children.some(c => c.changed);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return newItem;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** ------------------ CLOUDINARY UPLOAD ------------------ **/
|
|
101
160
|
async function uploadFileSigned(filePath, folder, roomId, token, email) {
|
|
102
161
|
const filename = path.basename(filePath);
|
|
103
162
|
|
|
@@ -137,26 +196,28 @@ async function uploadTree(fileTree, folderHex, roomId, token, email, parentPath
|
|
|
137
196
|
uploaded.push({
|
|
138
197
|
name: node.name,
|
|
139
198
|
type: "folder",
|
|
199
|
+
path: relativePath,
|
|
140
200
|
children
|
|
141
201
|
});
|
|
142
|
-
} else {
|
|
202
|
+
} else if (node.changed) {
|
|
143
203
|
const url = await uploadFileSigned(node.path, `tnc_uploads/${folderHex}`, roomId, token, email);
|
|
144
204
|
console.log(`π¦ Uploaded: ${relativePath} β ${url}`);
|
|
145
|
-
|
|
146
205
|
uploaded.push({
|
|
147
206
|
name: node.name,
|
|
148
207
|
type: "file",
|
|
149
208
|
path: relativePath,
|
|
150
209
|
size: node.size,
|
|
151
|
-
url
|
|
210
|
+
url
|
|
152
211
|
});
|
|
212
|
+
} else {
|
|
213
|
+
uploaded.push(node); // unchanged, no upload
|
|
153
214
|
}
|
|
154
215
|
}
|
|
155
216
|
|
|
156
217
|
return uploaded;
|
|
157
218
|
}
|
|
158
219
|
|
|
159
|
-
/**
|
|
220
|
+
/** ------------------ PUSH FUNCTION ------------------ **/
|
|
160
221
|
async function push(roomId, targetPath) {
|
|
161
222
|
const { token, email } = readToken();
|
|
162
223
|
const stats = fs.statSync(targetPath);
|
|
@@ -170,7 +231,7 @@ async function push(roomId, targetPath) {
|
|
|
170
231
|
const relativePath = path.basename(targetPath);
|
|
171
232
|
content = shouldIgnore(relativePath, ignoreList)
|
|
172
233
|
? []
|
|
173
|
-
: [{ name: relativePath, type: "file", path:
|
|
234
|
+
: [{ name: relativePath, type: "file", path: relativePath, size: stats.size }];
|
|
174
235
|
}
|
|
175
236
|
|
|
176
237
|
if (!content.length) {
|
|
@@ -178,11 +239,20 @@ async function push(roomId, targetPath) {
|
|
|
178
239
|
return;
|
|
179
240
|
}
|
|
180
241
|
|
|
242
|
+
const previousVersions = loadVersions();
|
|
243
|
+
const contentWithChanges = checkChanges(content, previousVersions);
|
|
244
|
+
const hasChanges = contentWithChanges.some(item => item.changed || (item.children && item.children.some(c => c.changed)));
|
|
245
|
+
|
|
246
|
+
if (!hasChanges) {
|
|
247
|
+
console.log("βΉοΈ No changes detected since last push.");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
181
251
|
try {
|
|
182
252
|
const folderHex = crypto.createHash("md5").update(path.basename(targetPath) + Date.now()).digest("hex");
|
|
183
253
|
|
|
184
254
|
console.log("π Uploading to Cloudinary...");
|
|
185
|
-
const uploadedTree = await uploadTree(
|
|
255
|
+
const uploadedTree = await uploadTree(contentWithChanges, folderHex, roomId, token, email);
|
|
186
256
|
|
|
187
257
|
console.log("ποΈ Sending metadata to backend...");
|
|
188
258
|
await axios.post(
|
|
@@ -192,12 +262,24 @@ async function push(roomId, targetPath) {
|
|
|
192
262
|
);
|
|
193
263
|
|
|
194
264
|
console.log("β
Upload complete! Metadata stored successfully.");
|
|
265
|
+
|
|
266
|
+
// Save version hashes
|
|
267
|
+
const flattenVersionMap = {};
|
|
268
|
+
const flatten = items => {
|
|
269
|
+
for (const item of items) {
|
|
270
|
+
flattenVersionMap[item.path || item.name] = item.hash;
|
|
271
|
+
if (item.children) flatten(item.children);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
flatten(contentWithChanges);
|
|
275
|
+
saveVersions(flattenVersionMap);
|
|
276
|
+
|
|
195
277
|
} catch (err) {
|
|
196
278
|
console.error("β Upload failed:", err.response?.data || err.message);
|
|
197
279
|
}
|
|
198
280
|
}
|
|
199
281
|
|
|
200
|
-
/**
|
|
282
|
+
/** ------------------ CLI HANDLER ------------------ **/
|
|
201
283
|
const args = process.argv.slice(2);
|
|
202
284
|
|
|
203
285
|
switch (args[0]) {
|
|
@@ -205,6 +287,10 @@ switch (args[0]) {
|
|
|
205
287
|
login();
|
|
206
288
|
break;
|
|
207
289
|
|
|
290
|
+
case "logout":
|
|
291
|
+
logout();
|
|
292
|
+
break;
|
|
293
|
+
|
|
208
294
|
case "push": {
|
|
209
295
|
const roomIndex = args.indexOf("--room");
|
|
210
296
|
if (roomIndex === -1 || !args[roomIndex + 1] || !args[roomIndex + 2]) {
|
|
@@ -222,4 +308,5 @@ switch (args[0]) {
|
|
|
222
308
|
console.log("Commands:");
|
|
223
309
|
console.log(" tnc login");
|
|
224
310
|
console.log(" tnc push --room <roomId> <path>");
|
|
311
|
+
console.log(" tnc logout");
|
|
225
312
|
}
|
package/branch/init.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import inquirer from "inquirer";
|
|
5
|
+
import axios from "axios";
|
|
6
|
+
const CWD = process.cwd();
|
|
7
|
+
|
|
8
|
+
async function projectInit() {
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const answer = await inquirer.prompt([
|
|
12
|
+
{type: "input", name: "projectName", message: "Enter Project Name:"},
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const HomeDir = os.homedir();
|
|
18
|
+
const data = fs.readFileSync(path.join(HomeDir, ".tncrc"));
|
|
19
|
+
const currentUser = JSON.parse(data).email;
|
|
20
|
+
// let us make the a .tncmeta.json file for keeping track of branch info
|
|
21
|
+
//.tncmeta.json should be in the a separate folder at the root of the project
|
|
22
|
+
|
|
23
|
+
const response = await axios.post("http://localhost:3001/cli/init", {
|
|
24
|
+
projectName : answer.projectName,
|
|
25
|
+
owner: currentUser
|
|
26
|
+
})
|
|
27
|
+
const projectId = response.data.project._id;
|
|
28
|
+
|
|
29
|
+
const tncFolder = fs.mkdirSync(path.join(CWD, ".tnc"), { recursive: true });
|
|
30
|
+
|
|
31
|
+
const metaFilePath = path.join(tncFolder, ".tncmeta.json");
|
|
32
|
+
|
|
33
|
+
fs.writeFileSync(metaFilePath, JSON.stringify({"projectId": projectId,
|
|
34
|
+
"projectName": answer.projectName,
|
|
35
|
+
"currentBranch": "main",
|
|
36
|
+
"lastCommit": null,
|
|
37
|
+
"files": {},
|
|
38
|
+
},
|
|
39
|
+
null,
|
|
40
|
+
2
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
console.log("β
Project initialized successfully!");
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
projectInit();
|
package/branch/pull.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
import axios from "axios";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import { createUploader } from "./cloudUploader.js";
|
|
8
|
+
|
|
9
|
+
const BASE_URL = "http://localhost:3001";
|
|
10
|
+
const CWD = process.cwd();
|
|
11
|
+
|
|
12
|
+
function readToken() {
|
|
13
|
+
const homeDir = os.homedir();
|
|
14
|
+
return JSON.parse(fs.readFileSync(path.join(homeDir, ".tncrc")));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readMeta() {
|
|
18
|
+
const metaPath = path.join(CWD, ".tnc", ".tncmeta.json");
|
|
19
|
+
return fs.existsSync(metaPath) ? JSON.parse(fs.readFileSync(metaPath)) : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeMeta(meta) {
|
|
23
|
+
const metaPath = path.join(CWD, ".tnc", ".tncmeta.json");
|
|
24
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ========== IGNORE HANDLING ==========
|
|
28
|
+
function loadIgnore(folderPath) {
|
|
29
|
+
const ignoreFile = path.join(folderPath, ".ignoretnc");
|
|
30
|
+
if (!fs.existsSync(ignoreFile)) return [];
|
|
31
|
+
return fs.readFileSync(ignoreFile, "utf-8")
|
|
32
|
+
.split("\n")
|
|
33
|
+
.map(l => l.trim())
|
|
34
|
+
.filter(l => l && !l.startsWith("#"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function shouldIgnore(relativePath, ignoreList) {
|
|
38
|
+
relativePath = relativePath.replace(/\\/g, "/");
|
|
39
|
+
return ignoreList.some(pattern => {
|
|
40
|
+
pattern = pattern.replace(/\\/g, "/");
|
|
41
|
+
if (pattern.endsWith("/**")) {
|
|
42
|
+
const folder = pattern.slice(0, -3);
|
|
43
|
+
return relativePath === folder || relativePath.startsWith(folder + "/");
|
|
44
|
+
}
|
|
45
|
+
if (pattern.startsWith("*.")) return relativePath.endsWith(pattern.slice(1));
|
|
46
|
+
return relativePath === pattern;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ========== SCAN FOLDER ==========
|
|
51
|
+
function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
|
|
52
|
+
const items = fs.readdirSync(folderPath, { withFileTypes: true });
|
|
53
|
+
const result = [];
|
|
54
|
+
for (const item of items) {
|
|
55
|
+
const fullPath = path.join(folderPath, item.name);
|
|
56
|
+
const relativePath = path.relative(rootPath, fullPath).replace(/\\/g, "/");
|
|
57
|
+
if (shouldIgnore(relativePath, ignoreList)) continue;
|
|
58
|
+
|
|
59
|
+
if (item.isDirectory()) {
|
|
60
|
+
result.push({ name: item.name, type: "folder", children: scanFolder(fullPath, ignoreList, rootPath) });
|
|
61
|
+
} else {
|
|
62
|
+
const stats = fs.statSync(fullPath);
|
|
63
|
+
result.push({ name: item.name, type: "file", path: fullPath, size: stats.size });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ========== PUSH EVERYTHING ==========
|
|
70
|
+
export async function dpushAll() {
|
|
71
|
+
const { token, email } = readToken();
|
|
72
|
+
const meta = readMeta();
|
|
73
|
+
if (!meta) return console.log("β Project not initialized locally. Run init first.");
|
|
74
|
+
const { projectId, currentBranch } = meta;
|
|
75
|
+
|
|
76
|
+
const stats = fs.statSync(CWD);
|
|
77
|
+
const ignoreList = loadIgnore(CWD);
|
|
78
|
+
|
|
79
|
+
const content = scanFolder(CWD, ignoreList);
|
|
80
|
+
if (!content.length) return console.log("β οΈ Nothing to upload (all ignored).");
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const uploader = createUploader(BASE_URL, token, email);
|
|
84
|
+
const folderHex = crypto.createHash("md5").update(path.basename(CWD) + Date.now()).digest("hex");
|
|
85
|
+
|
|
86
|
+
console.log("π Uploading entire project...");
|
|
87
|
+
const uploadedTree = await uploader.uploadTree(content, folderHex, projectId);
|
|
88
|
+
|
|
89
|
+
// Prepare metadata
|
|
90
|
+
const filesMetadata = [];
|
|
91
|
+
function flattenTree(nodes, parent = "") {
|
|
92
|
+
for (const node of nodes) {
|
|
93
|
+
const relPath = path.join(parent, node.name).replace(/\\/g, "/");
|
|
94
|
+
if (node.type === "file") {
|
|
95
|
+
const hash = crypto.createHash("md5").update(fs.readFileSync(path.join(CWD, relPath))).digest("hex");
|
|
96
|
+
filesMetadata.push({ filename: relPath, contentHash: hash, path: node.url, version: 0 });
|
|
97
|
+
} else if (node.type === "folder") {
|
|
98
|
+
flattenTree(node.children, relPath);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
flattenTree(uploadedTree);
|
|
103
|
+
|
|
104
|
+
console.log("ποΈ Sending metadata to backend...");
|
|
105
|
+
const pushRes = await axios.post(
|
|
106
|
+
`${BASE_URL}/cli/push`,
|
|
107
|
+
{ projectId, branchName: currentBranch, files: filesMetadata, author: email, message: "Full project push" },
|
|
108
|
+
{ headers: { authorization: `Bearer ${token}`, email } }
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
console.log("β
Push successful:", pushRes.data);
|
|
112
|
+
|
|
113
|
+
// Update local meta
|
|
114
|
+
meta.lastCommit = pushRes.data.commitId;
|
|
115
|
+
for (const f of pushRes.data.files) meta.files[f.filename] = f.version;
|
|
116
|
+
writeMeta(meta);
|
|
117
|
+
console.log("π Local metadata updated.");
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error("β Push failed:", err.response?.data || err.message);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Call push all
|
|
124
|
+
dpushAll();
|