thinkncollab-cli 0.0.24 → 0.0.26
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/bin/index.js +17 -0
- package/commands/pull.js +184 -95
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import crypto from "crypto";
|
|
|
8
8
|
import FormData from "form-data";
|
|
9
9
|
import projectInit from "../commands/init.js";
|
|
10
10
|
import Status from "../commands/status.js";
|
|
11
|
+
import pull from "../commands/pull.js";
|
|
11
12
|
|
|
12
13
|
const RC_FILE = path.join(os.homedir(), ".tncrc");
|
|
13
14
|
const VERSION_FILE = path.join(process.cwd(), ".tncversions");
|
|
@@ -414,6 +415,22 @@ async function main() {
|
|
|
414
415
|
case "init":
|
|
415
416
|
await projectInit();
|
|
416
417
|
break;
|
|
418
|
+
|
|
419
|
+
case "pull": {
|
|
420
|
+
const roomIndex = args.indexOf("--room");
|
|
421
|
+
const versionIndex = args.indexOf("--version");
|
|
422
|
+
|
|
423
|
+
if (roomIndex === -1 || !args[roomIndex + 1]) {
|
|
424
|
+
console.error("Usage: tnc pull --room <roomId> [--version <version>]");
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const roomId = args[roomIndex + 1];
|
|
429
|
+
const version = versionIndex !== -1 && args[versionIndex + 1] ? args[versionIndex + 1] : "latest";
|
|
430
|
+
|
|
431
|
+
await pull(roomId, version);
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
417
434
|
|
|
418
435
|
default:
|
|
419
436
|
console.log("✅ TNC CLI ready!");
|
package/commands/pull.js
CHANGED
|
@@ -1,124 +1,213 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
import fs from "fs";
|
|
3
2
|
import path from "path";
|
|
4
|
-
import crypto from "crypto";
|
|
5
3
|
import axios from "axios";
|
|
4
|
+
import https from "https";
|
|
6
5
|
import os from "os";
|
|
7
|
-
import { createUploader } from "./cloudUploader.js";
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
|
|
8
|
+
const RC_FILE = path.join(os.homedir(), ".tncrc");
|
|
10
9
|
const CWD = process.cwd();
|
|
11
10
|
|
|
11
|
+
/** ------------------ TOKEN UTILS ------------------ **/
|
|
12
12
|
function readToken() {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
if (!fs.existsSync(RC_FILE)) {
|
|
14
|
+
console.error("❌ Not logged in. Run 'tnc login' first.");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const data = JSON.parse(fs.readFileSync(RC_FILE));
|
|
18
|
+
return { token: data.token, email: data.email };
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
+
/** ------------------ DOWNLOAD FILE ------------------ **/
|
|
22
|
+
async function downloadFile(url, filePath) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const fileDir = path.dirname(filePath);
|
|
25
|
+
|
|
26
|
+
// Create directory if it doesn't exist
|
|
27
|
+
if (!fs.existsSync(fileDir)) {
|
|
28
|
+
fs.mkdirSync(fileDir, { recursive: true });
|
|
29
|
+
}
|
|
21
30
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
const file = fs.createWriteStream(filePath);
|
|
32
|
+
|
|
33
|
+
https.get(url, (response) => {
|
|
34
|
+
if (response.statusCode !== 200) {
|
|
35
|
+
reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
26
38
|
|
|
27
|
-
|
|
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
|
-
}
|
|
39
|
+
response.pipe(file);
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return relativePath === pattern;
|
|
41
|
+
file.on('finish', () => {
|
|
42
|
+
file.close();
|
|
43
|
+
resolve(filePath);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
}).on('error', (err) => {
|
|
47
|
+
fs.unlink(filePath, () => {}); // Delete the file on error
|
|
48
|
+
reject(err);
|
|
49
|
+
});
|
|
47
50
|
});
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (item.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
/** ------------------ PROCESS FOLDER CONTENT ------------------ **/
|
|
54
|
+
async function processFolderContent(content, basePath = "") {
|
|
55
|
+
let downloadedCount = 0;
|
|
56
|
+
let skippedCount = 0;
|
|
57
|
+
let errorCount = 0;
|
|
58
|
+
|
|
59
|
+
for (const item of content) {
|
|
60
|
+
const itemPath = path.join(basePath, item.path || item.name);
|
|
61
|
+
|
|
62
|
+
if (item.type === "folder") {
|
|
63
|
+
// Create directory
|
|
64
|
+
if (!fs.existsSync(itemPath)) {
|
|
65
|
+
fs.mkdirSync(itemPath, { recursive: true });
|
|
66
|
+
console.log(`📁 Created directory: ${itemPath}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Process children recursively
|
|
70
|
+
if (item.children && item.children.length > 0) {
|
|
71
|
+
const result = await processFolderContent(item.children, basePath);
|
|
72
|
+
downloadedCount += result.downloadedCount;
|
|
73
|
+
skippedCount += result.skippedCount;
|
|
74
|
+
errorCount += result.errorCount;
|
|
75
|
+
}
|
|
76
|
+
} else if (item.type === "file" && item.url) {
|
|
77
|
+
try {
|
|
78
|
+
// Check if file already exists and has same content
|
|
79
|
+
if (fs.existsSync(itemPath)) {
|
|
80
|
+
const existingStats = fs.statSync(itemPath);
|
|
81
|
+
if (existingStats.size === item.size) {
|
|
82
|
+
console.log(`✅ Already exists: ${itemPath}`);
|
|
83
|
+
skippedCount++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(`📥 Downloading: ${itemPath}`);
|
|
89
|
+
await downloadFile(item.url, itemPath);
|
|
90
|
+
console.log(`✅ Downloaded: ${itemPath}`);
|
|
91
|
+
downloadedCount++;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error(`❌ Failed to download ${itemPath}:`, err.message);
|
|
94
|
+
errorCount++;
|
|
95
|
+
}
|
|
96
|
+
} else if (item.type === "file" && !item.url) {
|
|
97
|
+
console.log(`⚠️ No URL available for: ${itemPath}`);
|
|
98
|
+
skippedCount++;
|
|
64
99
|
}
|
|
65
100
|
}
|
|
66
|
-
|
|
101
|
+
|
|
102
|
+
return { downloadedCount, skippedCount, errorCount };
|
|
67
103
|
}
|
|
68
104
|
|
|
69
|
-
|
|
70
|
-
export async function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
105
|
+
/** ------------------ PULL FUNCTION ------------------ **/
|
|
106
|
+
export default async function pull(roomId, version = "latest") {
|
|
107
|
+
try {
|
|
108
|
+
const { token, email } = readToken();
|
|
109
|
+
|
|
110
|
+
const tncMetaPath = path.join(process.cwd(), ".tnc", ".tncmeta.json");
|
|
111
|
+
if (!fs.existsSync(tncMetaPath)) {
|
|
112
|
+
console.error("❌ Project not initialized. Run 'tnc init' first.");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
75
115
|
|
|
76
|
-
|
|
77
|
-
|
|
116
|
+
const meta = JSON.parse(fs.readFileSync(tncMetaPath, "utf-8"));
|
|
117
|
+
const projectId = meta.projectId;
|
|
78
118
|
|
|
79
|
-
|
|
80
|
-
|
|
119
|
+
console.log(`🚀 Pulling files from room ${roomId}...`);
|
|
120
|
+
console.log(`📋 Project: ${projectId}`);
|
|
121
|
+
console.log(`🎯 Version: ${version}`);
|
|
122
|
+
console.log("");
|
|
81
123
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
}
|
|
124
|
+
// Get folder data from server
|
|
125
|
+
let url = `http://localhost:3001/folders/${roomId}/download`;
|
|
126
|
+
const params = { projectId };
|
|
127
|
+
|
|
128
|
+
if (version !== "latest") {
|
|
129
|
+
params.version = version;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const response = await axios.get(url, {
|
|
133
|
+
params,
|
|
134
|
+
headers: {
|
|
135
|
+
authorization: `Bearer ${token}`,
|
|
136
|
+
email: email
|
|
100
137
|
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const folderData = response.data;
|
|
141
|
+
|
|
142
|
+
if (!folderData || !folderData.rootContent) {
|
|
143
|
+
console.log("❌ No content found to download.");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`📦 Found version ${folderData.version} with ${folderData.rootContent.length} items`);
|
|
148
|
+
console.log("");
|
|
149
|
+
|
|
150
|
+
// Create backup of current versions file if it exists
|
|
151
|
+
const versionsFile = path.join(process.cwd(), ".tncversions");
|
|
152
|
+
if (fs.existsSync(versionsFile)) {
|
|
153
|
+
const backupPath = path.join(process.cwd(), `.tncversions.backup.${Date.now()}`);
|
|
154
|
+
fs.copyFileSync(versionsFile, backupPath);
|
|
155
|
+
console.log(`💾 Backed up existing versions to: ${path.basename(backupPath)}`);
|
|
101
156
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
console.log(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
157
|
+
|
|
158
|
+
// Process and download files
|
|
159
|
+
const result = await processFolderContent(folderData.rootContent, process.cwd());
|
|
160
|
+
|
|
161
|
+
console.log("");
|
|
162
|
+
console.log("📊 Download Summary:");
|
|
163
|
+
console.log("====================");
|
|
164
|
+
console.log(`✅ Downloaded: ${result.downloadedCount} files`);
|
|
165
|
+
console.log(`📋 Skipped: ${result.skippedCount} files (already exist)`);
|
|
166
|
+
console.log(`❌ Errors: ${result.errorCount} files`);
|
|
167
|
+
|
|
168
|
+
if (result.errorCount > 0) {
|
|
169
|
+
console.log("");
|
|
170
|
+
console.log("⚠️ Some files failed to download. Check the errors above.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Update local versions file with pulled data
|
|
174
|
+
const newVersionMap = {};
|
|
175
|
+
const flattenContent = (items) => {
|
|
176
|
+
for (const item of items) {
|
|
177
|
+
if (item.type === "file") {
|
|
178
|
+
newVersionMap[item.path] = {
|
|
179
|
+
hash: item.contentHash || item.hash,
|
|
180
|
+
url: item.url,
|
|
181
|
+
version: item.version || 1,
|
|
182
|
+
size: item.size || 0
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (item.children) {
|
|
186
|
+
flattenContent(item.children);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
flattenContent(folderData.rootContent);
|
|
192
|
+
fs.writeFileSync(versionsFile, JSON.stringify(newVersionMap, null, 2));
|
|
193
|
+
|
|
194
|
+
console.log("");
|
|
195
|
+
console.log(`💾 Updated local versions file with ${Object.keys(newVersionMap).length} files`);
|
|
196
|
+
console.log(`🎉 Pull completed successfully!`);
|
|
197
|
+
|
|
118
198
|
} catch (err) {
|
|
119
|
-
|
|
199
|
+
if (err.response) {
|
|
200
|
+
console.error("❌ Pull failed:", err.response.data.message || err.response.statusText);
|
|
201
|
+
} else {
|
|
202
|
+
console.error("❌ Pull failed:", err.message);
|
|
203
|
+
}
|
|
120
204
|
}
|
|
121
205
|
}
|
|
122
206
|
|
|
123
|
-
|
|
124
|
-
|
|
207
|
+
/** ------------------ PULL WITH OPTIONS ------------------ **/
|
|
208
|
+
export async function pullWithOptions(roomId, options = {}) {
|
|
209
|
+
const version = options.version || "latest";
|
|
210
|
+
const targetPath = options.targetPath || process.cwd();
|
|
211
|
+
|
|
212
|
+
await pull(roomId, version, targetPath);
|
|
213
|
+
}
|