thinkncollab-cli 0.0.23 → 0.0.24
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 +4 -0
- package/commands/status.js +284 -0
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import path from "path";
|
|
|
7
7
|
import crypto from "crypto";
|
|
8
8
|
import FormData from "form-data";
|
|
9
9
|
import projectInit from "../commands/init.js";
|
|
10
|
+
import Status from "../commands/status.js";
|
|
10
11
|
|
|
11
12
|
const RC_FILE = path.join(os.homedir(), ".tncrc");
|
|
12
13
|
const VERSION_FILE = path.join(process.cwd(), ".tncversions");
|
|
@@ -406,6 +407,9 @@ async function main() {
|
|
|
406
407
|
await push(roomId, targetPath);
|
|
407
408
|
break;
|
|
408
409
|
}
|
|
410
|
+
case "status":
|
|
411
|
+
await Status();
|
|
412
|
+
break;
|
|
409
413
|
|
|
410
414
|
case "init":
|
|
411
415
|
await projectInit();
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
|
|
6
|
+
const VERSION_FILE = path.join(process.cwd(), ".tncversions");
|
|
7
|
+
const CWD = process.cwd();
|
|
8
|
+
|
|
9
|
+
/** ------------------ IGNORE HANDLING ------------------ **/
|
|
10
|
+
function loadIgnore(folderPath) {
|
|
11
|
+
const ignoreFile = path.join(folderPath, ".ignoretnc");
|
|
12
|
+
if (!fs.existsSync(ignoreFile)) return [];
|
|
13
|
+
return fs
|
|
14
|
+
.readFileSync(ignoreFile, "utf-8")
|
|
15
|
+
.split("\n")
|
|
16
|
+
.map(line => line.trim())
|
|
17
|
+
.filter(line => line && !line.startsWith("#"));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function shouldIgnore(relativePath, ignoreList) {
|
|
21
|
+
return ignoreList.some(pattern => {
|
|
22
|
+
if (pattern.endsWith("/**")) {
|
|
23
|
+
const folder = pattern.slice(0, -3);
|
|
24
|
+
return relativePath === folder || relativePath.startsWith(folder + path.sep);
|
|
25
|
+
}
|
|
26
|
+
if (pattern.startsWith("*.")) {
|
|
27
|
+
return relativePath.endsWith(pattern.slice(1));
|
|
28
|
+
}
|
|
29
|
+
return relativePath === pattern;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** ------------------ SCAN FOLDER ------------------ **/
|
|
34
|
+
function scanFolder(folderPath, ignoreList, rootPath = folderPath) {
|
|
35
|
+
const items = fs.readdirSync(folderPath, { withFileTypes: true });
|
|
36
|
+
const result = [];
|
|
37
|
+
for (const item of items) {
|
|
38
|
+
const fullPath = path.join(folderPath, item.name);
|
|
39
|
+
const relativePath = path.relative(rootPath, fullPath);
|
|
40
|
+
|
|
41
|
+
if (shouldIgnore(relativePath, ignoreList)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (item.isDirectory()) {
|
|
46
|
+
result.push({
|
|
47
|
+
name: item.name,
|
|
48
|
+
type: "folder",
|
|
49
|
+
children: scanFolder(fullPath, ignoreList, rootPath),
|
|
50
|
+
path: relativePath
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
const stats = fs.statSync(fullPath);
|
|
54
|
+
result.push({
|
|
55
|
+
name: item.name,
|
|
56
|
+
type: "file",
|
|
57
|
+
path: relativePath,
|
|
58
|
+
size: stats.size
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** ------------------ VERSIONING ------------------ **/
|
|
66
|
+
function loadVersions() {
|
|
67
|
+
if (!fs.existsSync(VERSION_FILE)) return {};
|
|
68
|
+
const data = JSON.parse(fs.readFileSync(VERSION_FILE, "utf-8"));
|
|
69
|
+
|
|
70
|
+
// Backward compatibility: if old format (just strings), convert to new format
|
|
71
|
+
const converted = {};
|
|
72
|
+
for (const [path, value] of Object.entries(data)) {
|
|
73
|
+
if (typeof value === 'string') {
|
|
74
|
+
converted[path] = { hash: value, url: '', version: 1 };
|
|
75
|
+
} else {
|
|
76
|
+
converted[path] = value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return converted;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function computeFileHash(filePath) {
|
|
83
|
+
try {
|
|
84
|
+
if (!fs.existsSync(filePath)) return null;
|
|
85
|
+
|
|
86
|
+
const stats = fs.statSync(filePath);
|
|
87
|
+
if (stats.isDirectory()) {
|
|
88
|
+
// For folders: hash of sorted file names + their hashes
|
|
89
|
+
const items = fs.readdirSync(filePath).sort();
|
|
90
|
+
const childrenHashes = items.map(name =>
|
|
91
|
+
computeFileHash(path.join(filePath, name))
|
|
92
|
+
).filter(Boolean).join('|');
|
|
93
|
+
|
|
94
|
+
return crypto.createHash("sha256")
|
|
95
|
+
.update(`folder:${filePath}|${childrenHashes}`)
|
|
96
|
+
.digest("hex");
|
|
97
|
+
} else {
|
|
98
|
+
// For files: hash of content + file metadata
|
|
99
|
+
const content = fs.readFileSync(filePath);
|
|
100
|
+
const stats = fs.statSync(filePath);
|
|
101
|
+
|
|
102
|
+
return crypto.createHash("sha256")
|
|
103
|
+
.update(content)
|
|
104
|
+
.update(`size:${stats.size}|mtime:${stats.mtimeMs}`)
|
|
105
|
+
.digest("hex");
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error(`❌ Error computing hash for ${filePath}:`, err.message);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function checkChanges(fileTree, versionMap, rootPath = CWD) {
|
|
114
|
+
return fileTree.map(item => {
|
|
115
|
+
const fullPath = path.join(rootPath, item.path || item.name);
|
|
116
|
+
const relativePath = item.path || item.name;
|
|
117
|
+
|
|
118
|
+
let hash = null;
|
|
119
|
+
let changed = true;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
if (fs.existsSync(fullPath)) {
|
|
123
|
+
hash = computeFileHash(fullPath);
|
|
124
|
+
|
|
125
|
+
// Check against previous version
|
|
126
|
+
const prevVersion = versionMap[relativePath];
|
|
127
|
+
if (prevVersion && prevVersion.hash === hash) {
|
|
128
|
+
changed = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error(`❌ Error checking changes for ${relativePath}:`, err.message);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const newItem = {
|
|
136
|
+
...item,
|
|
137
|
+
changed,
|
|
138
|
+
hash,
|
|
139
|
+
path: relativePath
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Recursively check children for folders
|
|
143
|
+
if (item.type === "folder" && item.children && item.children.length > 0) {
|
|
144
|
+
newItem.children = checkChanges(item.children, versionMap, rootPath);
|
|
145
|
+
newItem.changed = newItem.changed || newItem.children.some(c => c.changed);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return newItem;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** ------------------ STATUS FUNCTION ------------------ **/
|
|
153
|
+
export default async function status() {
|
|
154
|
+
try {
|
|
155
|
+
const tncMetaPath = path.join(process.cwd(), ".tnc", ".tncmeta.json");
|
|
156
|
+
if (!fs.existsSync(tncMetaPath)) {
|
|
157
|
+
console.error("❌ Project not initialized. Run 'tnc init' first.");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const meta = JSON.parse(fs.readFileSync(tncMetaPath, "utf-8"));
|
|
162
|
+
const projectId = meta.projectId;
|
|
163
|
+
|
|
164
|
+
console.log("📊 TNC Project Status");
|
|
165
|
+
console.log("=====================");
|
|
166
|
+
console.log(`Project ID: ${projectId}`);
|
|
167
|
+
console.log(`Directory: ${process.cwd()}`);
|
|
168
|
+
console.log("");
|
|
169
|
+
|
|
170
|
+
// Check if versions file exists
|
|
171
|
+
if (!fs.existsSync(VERSION_FILE)) {
|
|
172
|
+
console.log("📭 No previous versions found. Run 'tnc push' first.");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const previousVersions = loadVersions();
|
|
177
|
+
const totalFiles = Object.keys(previousVersions).length;
|
|
178
|
+
|
|
179
|
+
console.log(`📁 Total tracked files: ${totalFiles}`);
|
|
180
|
+
console.log("");
|
|
181
|
+
|
|
182
|
+
// Scan current directory for changes
|
|
183
|
+
const ignoreList = loadIgnore(process.cwd());
|
|
184
|
+
const currentContent = scanFolder(process.cwd(), ignoreList);
|
|
185
|
+
const contentWithChanges = checkChanges(currentContent, previousVersions);
|
|
186
|
+
|
|
187
|
+
// Analyze changes
|
|
188
|
+
let changedCount = 0;
|
|
189
|
+
let newCount = 0;
|
|
190
|
+
let unchangedCount = 0;
|
|
191
|
+
let ignoredCount = 0;
|
|
192
|
+
|
|
193
|
+
const flattenAndAnalyze = (items) => {
|
|
194
|
+
for (const item of items) {
|
|
195
|
+
if (item.type === "file") {
|
|
196
|
+
if (!previousVersions[item.path]) {
|
|
197
|
+
newCount++;
|
|
198
|
+
} else if (item.changed) {
|
|
199
|
+
changedCount++;
|
|
200
|
+
} else {
|
|
201
|
+
unchangedCount++;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (item.children) {
|
|
205
|
+
flattenAndAnalyze(item.children);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
flattenAndAnalyze(contentWithChanges);
|
|
211
|
+
|
|
212
|
+
console.log("🔄 Change Summary:");
|
|
213
|
+
console.log(` ✅ Unchanged files: ${unchangedCount}`);
|
|
214
|
+
console.log(` 📝 Modified files: ${changedCount}`);
|
|
215
|
+
console.log(` 🆕 New files: ${newCount}`);
|
|
216
|
+
console.log("");
|
|
217
|
+
|
|
218
|
+
// Show detailed changes
|
|
219
|
+
if (changedCount > 0 || newCount > 0) {
|
|
220
|
+
console.log("📋 Detailed Changes:");
|
|
221
|
+
console.log("-------------------");
|
|
222
|
+
|
|
223
|
+
const showChanges = (items, indent = "") => {
|
|
224
|
+
for (const item of items) {
|
|
225
|
+
if (item.type === "file") {
|
|
226
|
+
if (!previousVersions[item.path]) {
|
|
227
|
+
console.log(`${indent}🆕 ${item.path} (new)`);
|
|
228
|
+
} else if (item.changed) {
|
|
229
|
+
console.log(`${indent}📝 ${item.path} (modified)`);
|
|
230
|
+
}
|
|
231
|
+
} else if (item.type === "folder") {
|
|
232
|
+
const hasFileChanges = item.children && item.children.some(child =>
|
|
233
|
+
child.type === 'file' && (!previousVersions[child.path] || child.changed)
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (hasFileChanges) {
|
|
237
|
+
console.log(`${indent}📁 ${item.path}/`);
|
|
238
|
+
if (item.children) {
|
|
239
|
+
showChanges(item.children, indent + " ");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
showChanges(contentWithChanges);
|
|
247
|
+
} else {
|
|
248
|
+
console.log("🎉 No changes detected. Everything is up to date!");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log("");
|
|
252
|
+
|
|
253
|
+
// Show version info
|
|
254
|
+
const versions = Object.values(previousVersions);
|
|
255
|
+
if (versions.length > 0) {
|
|
256
|
+
const maxVersion = Math.max(...versions.map(v => v.version || 1));
|
|
257
|
+
console.log(`🏷️ Latest version: ${maxVersion}`);
|
|
258
|
+
|
|
259
|
+
// Show file type breakdown
|
|
260
|
+
const extensions = {};
|
|
261
|
+
Object.keys(previousVersions).forEach(filePath => {
|
|
262
|
+
const ext = path.extname(filePath) || 'no extension';
|
|
263
|
+
extensions[ext] = (extensions[ext] || 0) + 1;
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
console.log("");
|
|
267
|
+
console.log("📄 File Types:");
|
|
268
|
+
Object.entries(extensions)
|
|
269
|
+
.sort(([,a], [,b]) => b - a)
|
|
270
|
+
.forEach(([ext, count]) => {
|
|
271
|
+
console.log(` ${ext || '(no ext)'}: ${count} files`);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Show last push info if available
|
|
276
|
+
const versionFileStats = fs.statSync(VERSION_FILE);
|
|
277
|
+
console.log("");
|
|
278
|
+
console.log(`⏰ Last push: ${versionFileStats.mtime.toLocaleString()}`);
|
|
279
|
+
|
|
280
|
+
} catch (err) {
|
|
281
|
+
console.error("❌ Error checking status:", err.message);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|