contextloop-cli 0.1.1 → 0.2.1
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/dist/index.js +1113 -53
- package/package.json +16 -16
package/dist/index.js
CHANGED
|
@@ -31,7 +31,10 @@ var CLIError = class extends Error {
|
|
|
31
31
|
AUTH_EXPIRED: 2,
|
|
32
32
|
PROJECT_NOT_SET: 6,
|
|
33
33
|
TIMEOUT: 12,
|
|
34
|
-
FILE_TOO_LARGE: 13
|
|
34
|
+
FILE_TOO_LARGE: 13,
|
|
35
|
+
SYNC_CONFLICT: 7,
|
|
36
|
+
SYNC_PARTIAL_FAILURE: 8,
|
|
37
|
+
VALIDATION_ERROR: 5
|
|
35
38
|
};
|
|
36
39
|
return exitCodes[this.code] ?? 1;
|
|
37
40
|
}
|
|
@@ -131,6 +134,7 @@ import * as crypto2 from "crypto";
|
|
|
131
134
|
import * as os from "os";
|
|
132
135
|
var SERVICE_NAME = "contextloop-cli";
|
|
133
136
|
var CREDENTIALS_FILENAME = "credentials.enc";
|
|
137
|
+
var LOCAL_CONFIG_DIR = ".contextloop";
|
|
134
138
|
var KEY_API_KEY = "api-key";
|
|
135
139
|
var KEY_ACCESS_TOKEN = "access-token";
|
|
136
140
|
var KEY_REFRESH_TOKEN = "refresh-token";
|
|
@@ -138,13 +142,32 @@ var ENCRYPTION_ALGORITHM = "aes-256-gcm";
|
|
|
138
142
|
var KEY_LENGTH = 32;
|
|
139
143
|
var IV_LENGTH = 16;
|
|
140
144
|
var AUTH_TAG_LENGTH = 16;
|
|
141
|
-
function
|
|
145
|
+
function getGlobalConfigDir() {
|
|
142
146
|
const home = os.homedir();
|
|
143
147
|
if (process.platform === "win32") {
|
|
144
148
|
return path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "contextloop-cli");
|
|
145
149
|
}
|
|
146
150
|
return path.join(process.env.XDG_CONFIG_HOME || path.join(home, ".config"), "contextloop-cli");
|
|
147
151
|
}
|
|
152
|
+
function findLocalConfigDir() {
|
|
153
|
+
let current = process.cwd();
|
|
154
|
+
const root = path.parse(current).root;
|
|
155
|
+
while (current !== root) {
|
|
156
|
+
const configDir = path.join(current, LOCAL_CONFIG_DIR);
|
|
157
|
+
if (fs.existsSync(configDir) && fs.statSync(configDir).isDirectory()) {
|
|
158
|
+
return configDir;
|
|
159
|
+
}
|
|
160
|
+
current = path.dirname(current);
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
function getOrCreateLocalConfigDir() {
|
|
165
|
+
const configDir = path.join(process.cwd(), LOCAL_CONFIG_DIR);
|
|
166
|
+
if (!fs.existsSync(configDir)) {
|
|
167
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
168
|
+
}
|
|
169
|
+
return configDir;
|
|
170
|
+
}
|
|
148
171
|
function getMachineKey() {
|
|
149
172
|
const machineId = `${os.hostname()}-${os.userInfo().username}-${SERVICE_NAME}`;
|
|
150
173
|
return crypto2.scryptSync(machineId, "contextloop-cli-salt", KEY_LENGTH);
|
|
@@ -217,11 +240,14 @@ var KeyringBackend = class {
|
|
|
217
240
|
}
|
|
218
241
|
};
|
|
219
242
|
var EncryptedFileBackend = class {
|
|
220
|
-
name
|
|
243
|
+
name;
|
|
221
244
|
configDir;
|
|
222
245
|
filePath;
|
|
223
|
-
|
|
224
|
-
|
|
246
|
+
scope;
|
|
247
|
+
constructor(scope = "global", configDir) {
|
|
248
|
+
this.scope = scope;
|
|
249
|
+
this.name = scope === "local" ? "encrypted-file-local" : "encrypted-file";
|
|
250
|
+
this.configDir = configDir ?? getGlobalConfigDir();
|
|
225
251
|
this.filePath = path.join(this.configDir, CREDENTIALS_FILENAME);
|
|
226
252
|
}
|
|
227
253
|
readStore() {
|
|
@@ -281,10 +307,16 @@ async function getBackend() {
|
|
|
281
307
|
backendInitialized = true;
|
|
282
308
|
return activeBackend;
|
|
283
309
|
}
|
|
284
|
-
async function storeApiKey(apiKey) {
|
|
310
|
+
async function storeApiKey(apiKey, local = false) {
|
|
285
311
|
try {
|
|
286
|
-
|
|
287
|
-
|
|
312
|
+
if (local) {
|
|
313
|
+
const configDir = getOrCreateLocalConfigDir();
|
|
314
|
+
const backend = new EncryptedFileBackend("local", configDir);
|
|
315
|
+
await backend.set(KEY_API_KEY, apiKey);
|
|
316
|
+
} else {
|
|
317
|
+
const backend = await getBackend();
|
|
318
|
+
await backend.set(KEY_API_KEY, apiKey);
|
|
319
|
+
}
|
|
288
320
|
} catch (error) {
|
|
289
321
|
throw new CLIError(
|
|
290
322
|
"KEYCHAIN_ERROR",
|
|
@@ -292,20 +324,44 @@ async function storeApiKey(apiKey) {
|
|
|
292
324
|
);
|
|
293
325
|
}
|
|
294
326
|
}
|
|
295
|
-
async function
|
|
327
|
+
async function getApiKeyWithScope() {
|
|
296
328
|
if (process.env.CONTEXTLOOP_API_KEY) {
|
|
297
|
-
return process.env.CONTEXTLOOP_API_KEY;
|
|
329
|
+
return { apiKey: process.env.CONTEXTLOOP_API_KEY, scope: "environment" };
|
|
330
|
+
}
|
|
331
|
+
const localDir = findLocalConfigDir();
|
|
332
|
+
if (localDir) {
|
|
333
|
+
try {
|
|
334
|
+
const localBackend = new EncryptedFileBackend("local", localDir);
|
|
335
|
+
const localKey = await localBackend.get(KEY_API_KEY);
|
|
336
|
+
if (localKey) {
|
|
337
|
+
return { apiKey: localKey, scope: "local" };
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
298
341
|
}
|
|
299
342
|
try {
|
|
300
343
|
const backend = await getBackend();
|
|
301
|
-
|
|
344
|
+
const key = await backend.get(KEY_API_KEY);
|
|
345
|
+
if (key) {
|
|
346
|
+
return { apiKey: key, scope: "global" };
|
|
347
|
+
}
|
|
302
348
|
} catch {
|
|
303
|
-
return null;
|
|
304
349
|
}
|
|
350
|
+
return { apiKey: null, scope: null };
|
|
351
|
+
}
|
|
352
|
+
async function getApiKey() {
|
|
353
|
+
const { apiKey } = await getApiKeyWithScope();
|
|
354
|
+
return apiKey;
|
|
305
355
|
}
|
|
306
|
-
async function storeTokens(accessToken, refreshToken) {
|
|
356
|
+
async function storeTokens(accessToken, refreshToken, local = false) {
|
|
307
357
|
try {
|
|
308
|
-
|
|
358
|
+
let backend;
|
|
359
|
+
if (local) {
|
|
360
|
+
const configDir = getOrCreateLocalConfigDir();
|
|
361
|
+
backend = new EncryptedFileBackend("local", configDir);
|
|
362
|
+
} else {
|
|
363
|
+
backend = await getBackend();
|
|
364
|
+
}
|
|
309
365
|
await backend.set(KEY_ACCESS_TOKEN, accessToken);
|
|
310
366
|
if (refreshToken) {
|
|
311
367
|
await backend.set(KEY_REFRESH_TOKEN, refreshToken);
|
|
@@ -317,32 +373,68 @@ async function storeTokens(accessToken, refreshToken) {
|
|
|
317
373
|
);
|
|
318
374
|
}
|
|
319
375
|
}
|
|
320
|
-
async function
|
|
376
|
+
async function getTokensWithScope() {
|
|
377
|
+
const localDir = findLocalConfigDir();
|
|
378
|
+
if (localDir) {
|
|
379
|
+
try {
|
|
380
|
+
const localBackend = new EncryptedFileBackend("local", localDir);
|
|
381
|
+
const accessToken = await localBackend.get(KEY_ACCESS_TOKEN);
|
|
382
|
+
if (accessToken) {
|
|
383
|
+
const refreshToken = await localBackend.get(KEY_REFRESH_TOKEN);
|
|
384
|
+
return { accessToken, refreshToken, scope: "local" };
|
|
385
|
+
}
|
|
386
|
+
} catch {
|
|
387
|
+
}
|
|
388
|
+
}
|
|
321
389
|
try {
|
|
322
390
|
const backend = await getBackend();
|
|
323
391
|
const accessToken = await backend.get(KEY_ACCESS_TOKEN);
|
|
324
|
-
|
|
325
|
-
|
|
392
|
+
if (accessToken) {
|
|
393
|
+
const refreshToken = await backend.get(KEY_REFRESH_TOKEN);
|
|
394
|
+
return { accessToken, refreshToken, scope: "global" };
|
|
395
|
+
}
|
|
326
396
|
} catch {
|
|
327
|
-
return { accessToken: null, refreshToken: null };
|
|
328
397
|
}
|
|
398
|
+
return { accessToken: null, refreshToken: null, scope: null };
|
|
329
399
|
}
|
|
330
|
-
async function
|
|
331
|
-
const
|
|
332
|
-
|
|
400
|
+
async function getTokens() {
|
|
401
|
+
const { accessToken, refreshToken } = await getTokensWithScope();
|
|
402
|
+
return { accessToken, refreshToken };
|
|
403
|
+
}
|
|
404
|
+
async function getCredentialsWithScope() {
|
|
405
|
+
const { apiKey, scope: apiKeyScope } = await getApiKeyWithScope();
|
|
406
|
+
const { accessToken, refreshToken, scope: tokenScope } = await getTokensWithScope();
|
|
407
|
+
const scope = apiKeyScope ?? tokenScope;
|
|
333
408
|
return {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
409
|
+
credentials: {
|
|
410
|
+
apiKey: apiKey ?? void 0,
|
|
411
|
+
accessToken: accessToken ?? void 0,
|
|
412
|
+
refreshToken: refreshToken ?? void 0
|
|
413
|
+
},
|
|
414
|
+
scope
|
|
337
415
|
};
|
|
338
416
|
}
|
|
339
|
-
async function clearCredentials() {
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
417
|
+
async function clearCredentials(scope = "all") {
|
|
418
|
+
if (scope === "local" || scope === "all") {
|
|
419
|
+
const localDir = findLocalConfigDir();
|
|
420
|
+
if (localDir) {
|
|
421
|
+
try {
|
|
422
|
+
const localBackend = new EncryptedFileBackend("local", localDir);
|
|
423
|
+
await localBackend.delete(KEY_API_KEY);
|
|
424
|
+
await localBackend.delete(KEY_ACCESS_TOKEN);
|
|
425
|
+
await localBackend.delete(KEY_REFRESH_TOKEN);
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (scope === "global" || scope === "all") {
|
|
431
|
+
try {
|
|
432
|
+
const backend = await getBackend();
|
|
433
|
+
await backend.delete(KEY_API_KEY);
|
|
434
|
+
await backend.delete(KEY_ACCESS_TOKEN);
|
|
435
|
+
await backend.delete(KEY_REFRESH_TOKEN);
|
|
436
|
+
} catch {
|
|
437
|
+
}
|
|
346
438
|
}
|
|
347
439
|
}
|
|
348
440
|
|
|
@@ -381,7 +473,7 @@ function createAPIClient(config2, logger) {
|
|
|
381
473
|
function sleep(ms) {
|
|
382
474
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
383
475
|
}
|
|
384
|
-
async function request(method,
|
|
476
|
+
async function request(method, path6, options = {}) {
|
|
385
477
|
const authHeader = await getAuthHeader();
|
|
386
478
|
const headers = {
|
|
387
479
|
"Content-Type": "application/json"
|
|
@@ -395,8 +487,8 @@ function createAPIClient(config2, logger) {
|
|
|
395
487
|
let lastError = null;
|
|
396
488
|
for (let attempt = 0; attempt <= RATE_LIMIT_CONFIG.maxRetries; attempt++) {
|
|
397
489
|
try {
|
|
398
|
-
logger.debug(`${method} ${
|
|
399
|
-
const response = await ofetch(`${baseURL}${
|
|
490
|
+
logger.debug(`${method} ${path6}`, { query, attempt });
|
|
491
|
+
const response = await ofetch(`${baseURL}${path6}`, {
|
|
400
492
|
method,
|
|
401
493
|
headers,
|
|
402
494
|
body: options.body,
|
|
@@ -508,9 +600,9 @@ function createAPIClient(config2, logger) {
|
|
|
508
600
|
`/cli/projects/${projectId}/documents/${documentId}`
|
|
509
601
|
);
|
|
510
602
|
},
|
|
511
|
-
async getDocumentByPath(projectId,
|
|
603
|
+
async getDocumentByPath(projectId, path6) {
|
|
512
604
|
return request("GET", `/cli/projects/${projectId}/documents`, {
|
|
513
|
-
query: { path:
|
|
605
|
+
query: { path: path6 }
|
|
514
606
|
});
|
|
515
607
|
},
|
|
516
608
|
async createDocument(projectId, data) {
|
|
@@ -699,6 +791,18 @@ import http from "http";
|
|
|
699
791
|
import { URL } from "url";
|
|
700
792
|
import open from "open";
|
|
701
793
|
import chalk2 from "chalk";
|
|
794
|
+
import { ofetch as ofetch2 } from "ofetch";
|
|
795
|
+
async function fetchProjectInfo(apiUrl, projectId, accessToken) {
|
|
796
|
+
try {
|
|
797
|
+
const project = await ofetch2(`${apiUrl}/cli/projects/${projectId}`, {
|
|
798
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
799
|
+
timeout: 1e4
|
|
800
|
+
});
|
|
801
|
+
return { name: project.name, slug: project.slug };
|
|
802
|
+
} catch {
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
702
806
|
var CALLBACK_PORT = 54320;
|
|
703
807
|
var CALLBACK_PATH = "/callback";
|
|
704
808
|
var AUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
@@ -881,7 +985,18 @@ ${authUrl.toString()}`);
|
|
|
881
985
|
if (tokens.project_id) {
|
|
882
986
|
saveConfig({ defaultProjectId: tokens.project_id });
|
|
883
987
|
console.log(chalk2.green("Successfully authenticated!"));
|
|
884
|
-
|
|
988
|
+
const projectInfo = await fetchProjectInfo(
|
|
989
|
+
config2.apiUrl,
|
|
990
|
+
tokens.project_id,
|
|
991
|
+
tokens.access_token
|
|
992
|
+
);
|
|
993
|
+
if (projectInfo) {
|
|
994
|
+
console.log(
|
|
995
|
+
chalk2.gray(`Default project set to ${chalk2.bold(projectInfo.name)} (${tokens.project_id})`)
|
|
996
|
+
);
|
|
997
|
+
} else {
|
|
998
|
+
console.log(chalk2.gray(`Default project set to: ${tokens.project_id}`));
|
|
999
|
+
}
|
|
885
1000
|
} else {
|
|
886
1001
|
console.log(chalk2.green("Successfully authenticated!"));
|
|
887
1002
|
}
|
|
@@ -964,7 +1079,18 @@ async function manualLogin(config2, logger) {
|
|
|
964
1079
|
if (tokens.project_id) {
|
|
965
1080
|
saveConfig({ defaultProjectId: tokens.project_id });
|
|
966
1081
|
console.log(chalk2.green("Successfully authenticated!"));
|
|
967
|
-
|
|
1082
|
+
const projectInfo = await fetchProjectInfo(
|
|
1083
|
+
config2.apiUrl,
|
|
1084
|
+
tokens.project_id,
|
|
1085
|
+
tokens.access_token
|
|
1086
|
+
);
|
|
1087
|
+
if (projectInfo) {
|
|
1088
|
+
console.log(
|
|
1089
|
+
chalk2.gray(`Default project set to ${chalk2.bold(projectInfo.name)} (${tokens.project_id})`)
|
|
1090
|
+
);
|
|
1091
|
+
} else {
|
|
1092
|
+
console.log(chalk2.gray(`Default project set to: ${tokens.project_id}`));
|
|
1093
|
+
}
|
|
968
1094
|
} else {
|
|
969
1095
|
console.log(chalk2.green("Successfully authenticated!"));
|
|
970
1096
|
}
|
|
@@ -1061,22 +1187,36 @@ function formatRelativeTime(date) {
|
|
|
1061
1187
|
// src/commands/auth.ts
|
|
1062
1188
|
function registerAuthCommands(program2, context2) {
|
|
1063
1189
|
const auth = program2.command("auth").description("Authentication commands");
|
|
1064
|
-
auth.command("login").description("Authenticate with ContextLoop").option("--api-key <key>", "Login with an API key").option("--manual", "Use manual login flow (for environments without browser)").action(async (options) => {
|
|
1190
|
+
auth.command("login").description("Authenticate with ContextLoop").option("--api-key <key>", "Login with an API key").option("--local", "Store credentials in project directory (.contextloop/)").option("--manual", "Use manual login flow (for environments without browser)").action(async (options) => {
|
|
1065
1191
|
const globalOptions = program2.opts();
|
|
1066
1192
|
try {
|
|
1193
|
+
const isLocal = options.local ?? false;
|
|
1194
|
+
const scopeLabel = isLocal ? "project" : "global";
|
|
1067
1195
|
if (options.apiKey) {
|
|
1068
1196
|
const spinner = ora("Validating API key...").start();
|
|
1069
1197
|
try {
|
|
1070
1198
|
const { user } = await context2.apiClient.validateApiKey(options.apiKey);
|
|
1071
|
-
await storeApiKey(options.apiKey);
|
|
1072
|
-
spinner.succeed(
|
|
1199
|
+
await storeApiKey(options.apiKey, isLocal);
|
|
1200
|
+
spinner.succeed(
|
|
1201
|
+
`Logged in as ${chalk4.bold(user.name)} (${user.email}) [${chalk4.cyan(scopeLabel)}]`
|
|
1202
|
+
);
|
|
1203
|
+
if (isLocal) {
|
|
1204
|
+
console.log(chalk4.gray("Credentials stored in .contextloop/ directory"));
|
|
1205
|
+
console.log(chalk4.yellow("Tip: Add .contextloop/ to your .gitignore"));
|
|
1206
|
+
}
|
|
1073
1207
|
} catch (error) {
|
|
1074
1208
|
spinner.fail("Login failed");
|
|
1075
1209
|
throw error;
|
|
1076
1210
|
}
|
|
1077
1211
|
} else if (options.manual) {
|
|
1212
|
+
if (isLocal) {
|
|
1213
|
+
console.log(chalk4.yellow("Note: OAuth login currently only supports global scope"));
|
|
1214
|
+
}
|
|
1078
1215
|
await manualLogin(context2.config, context2.logger);
|
|
1079
1216
|
} else {
|
|
1217
|
+
if (isLocal) {
|
|
1218
|
+
console.log(chalk4.yellow("Note: OAuth login currently only supports global scope"));
|
|
1219
|
+
}
|
|
1080
1220
|
await interactiveLogin(context2.config, context2.logger);
|
|
1081
1221
|
}
|
|
1082
1222
|
} catch (error) {
|
|
@@ -1084,12 +1224,21 @@ function registerAuthCommands(program2, context2) {
|
|
|
1084
1224
|
process.exit(getExitCode(error));
|
|
1085
1225
|
}
|
|
1086
1226
|
});
|
|
1087
|
-
auth.command("logout").description("Clear stored credentials").action(async () => {
|
|
1227
|
+
auth.command("logout").description("Clear stored credentials").option("--local", "Clear only project-local credentials").option("--global", "Clear only global credentials").option("--all", "Clear all credentials (default)").action(async (options) => {
|
|
1088
1228
|
const globalOptions = program2.opts();
|
|
1089
1229
|
try {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1230
|
+
let scope = "all";
|
|
1231
|
+
if (options.local && !options.global) {
|
|
1232
|
+
scope = "local";
|
|
1233
|
+
} else if (options.global && !options.local) {
|
|
1234
|
+
scope = "global";
|
|
1235
|
+
}
|
|
1236
|
+
await clearCredentials(scope);
|
|
1237
|
+
if (scope === "global" || scope === "all") {
|
|
1238
|
+
clearConfig(["defaultProjectId", "defaultProjectSlug"]);
|
|
1239
|
+
}
|
|
1240
|
+
const scopeMsg = scope === "local" ? "project-local" : scope === "global" ? "global" : "all";
|
|
1241
|
+
console.log(chalk4.green(`Logged out successfully (${scopeMsg} credentials cleared).`));
|
|
1093
1242
|
} catch (error) {
|
|
1094
1243
|
console.error(formatError(error, globalOptions.format));
|
|
1095
1244
|
process.exit(getExitCode(error));
|
|
@@ -1098,7 +1247,7 @@ function registerAuthCommands(program2, context2) {
|
|
|
1098
1247
|
auth.command("whoami").description("Display current user information").action(async () => {
|
|
1099
1248
|
const globalOptions = program2.opts();
|
|
1100
1249
|
try {
|
|
1101
|
-
const creds = await
|
|
1250
|
+
const { credentials: creds, scope } = await getCredentialsWithScope();
|
|
1102
1251
|
if (!creds.apiKey && !creds.accessToken) {
|
|
1103
1252
|
if (globalOptions.format === "json") {
|
|
1104
1253
|
console.log(JSON.stringify({ loggedIn: false }));
|
|
@@ -1113,27 +1262,56 @@ function registerAuthCommands(program2, context2) {
|
|
|
1113
1262
|
const spinner = ora("Fetching user info...").start();
|
|
1114
1263
|
try {
|
|
1115
1264
|
const user = await context2.apiClient.getCurrentUser();
|
|
1116
|
-
spinner.stop();
|
|
1117
1265
|
const defaultProjectId = context2.config.defaultProjectId;
|
|
1266
|
+
let project = null;
|
|
1267
|
+
if (defaultProjectId) {
|
|
1268
|
+
spinner.text = "Fetching project info...";
|
|
1269
|
+
try {
|
|
1270
|
+
project = await context2.apiClient.getProject(defaultProjectId);
|
|
1271
|
+
} catch {
|
|
1272
|
+
project = null;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
spinner.stop();
|
|
1276
|
+
const localDir = findLocalConfigDir();
|
|
1118
1277
|
printOutput(
|
|
1119
|
-
user,
|
|
1278
|
+
{ user, project, scope, localDir },
|
|
1120
1279
|
{
|
|
1121
|
-
text: (u) => {
|
|
1280
|
+
text: ({ user: u, project: p, scope: s, localDir: ld }) => {
|
|
1122
1281
|
const lines = [
|
|
1123
1282
|
chalk4.bold(u.name),
|
|
1124
1283
|
chalk4.gray(u.email),
|
|
1125
1284
|
chalk4.gray(`ID: ${u.id}`)
|
|
1126
1285
|
];
|
|
1127
|
-
|
|
1286
|
+
lines.push("");
|
|
1287
|
+
const scopeLabel = s === "environment" ? chalk4.magenta("environment variable") : s === "local" ? chalk4.cyan("project-local") : chalk4.blue("global");
|
|
1288
|
+
lines.push(`Auth: ${scopeLabel}`);
|
|
1289
|
+
if (s === "local" && ld) {
|
|
1290
|
+
lines.push(chalk4.gray(` ${ld}`));
|
|
1291
|
+
}
|
|
1292
|
+
if (p) {
|
|
1293
|
+
lines.push("");
|
|
1294
|
+
lines.push(chalk4.bold("Current project:"));
|
|
1295
|
+
lines.push(` ${chalk4.cyan(p.name)}`);
|
|
1296
|
+
lines.push(chalk4.gray(` ID: ${p.id}`));
|
|
1297
|
+
if (p.slug) {
|
|
1298
|
+
lines.push(chalk4.gray(` Slug: ${p.slug}`));
|
|
1299
|
+
}
|
|
1300
|
+
} else if (defaultProjectId) {
|
|
1128
1301
|
lines.push("");
|
|
1129
|
-
lines.push(chalk4.
|
|
1302
|
+
lines.push(chalk4.yellow(`Default project: ${defaultProjectId} (not found)`));
|
|
1303
|
+
} else {
|
|
1304
|
+
lines.push("");
|
|
1305
|
+
lines.push(chalk4.gray("No default project set."));
|
|
1306
|
+
lines.push(chalk4.gray("Run 'contextloop project use <id>' to set one."));
|
|
1130
1307
|
}
|
|
1131
1308
|
return lines.join("\n");
|
|
1132
1309
|
},
|
|
1133
|
-
json: (u) => ({
|
|
1310
|
+
json: ({ user: u, project: p, scope: s }) => ({
|
|
1134
1311
|
loggedIn: true,
|
|
1135
1312
|
user: u,
|
|
1136
|
-
|
|
1313
|
+
authScope: s,
|
|
1314
|
+
project: p ? { id: p.id, name: p.name, slug: p.slug, role: p.role } : null
|
|
1137
1315
|
})
|
|
1138
1316
|
},
|
|
1139
1317
|
globalOptions
|
|
@@ -2222,6 +2400,887 @@ Page ${r.pagination.page} of ${r.pagination.totalPages} (${r.pagination.totalIte
|
|
|
2222
2400
|
});
|
|
2223
2401
|
}
|
|
2224
2402
|
|
|
2403
|
+
// src/commands/sync.ts
|
|
2404
|
+
import ora6 from "ora";
|
|
2405
|
+
import chalk9 from "chalk";
|
|
2406
|
+
|
|
2407
|
+
// src/lib/sync/pool.ts
|
|
2408
|
+
async function executeWithPool(items, executor, options) {
|
|
2409
|
+
const { concurrency, onProgress } = options;
|
|
2410
|
+
const successes = [];
|
|
2411
|
+
const failures = [];
|
|
2412
|
+
if (items.length === 0) {
|
|
2413
|
+
return { successes, failures };
|
|
2414
|
+
}
|
|
2415
|
+
let completed = 0;
|
|
2416
|
+
let nextIndex = 0;
|
|
2417
|
+
return new Promise((resolve) => {
|
|
2418
|
+
const executing = /* @__PURE__ */ new Set();
|
|
2419
|
+
function processNext() {
|
|
2420
|
+
while (executing.size < concurrency && nextIndex < items.length) {
|
|
2421
|
+
const currentIndex = nextIndex++;
|
|
2422
|
+
const item = items[currentIndex];
|
|
2423
|
+
const promise = executor(item).then((result) => {
|
|
2424
|
+
successes.push({ item, result });
|
|
2425
|
+
}).catch((error) => {
|
|
2426
|
+
failures.push({
|
|
2427
|
+
item,
|
|
2428
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
2429
|
+
});
|
|
2430
|
+
}).finally(() => {
|
|
2431
|
+
executing.delete(promise);
|
|
2432
|
+
completed++;
|
|
2433
|
+
onProgress?.(completed, items.length);
|
|
2434
|
+
if (completed === items.length) {
|
|
2435
|
+
resolve({ successes, failures });
|
|
2436
|
+
} else {
|
|
2437
|
+
processNext();
|
|
2438
|
+
}
|
|
2439
|
+
});
|
|
2440
|
+
executing.add(promise);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
processNext();
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
async function mapWithPool(items, executor, options) {
|
|
2447
|
+
const { concurrency, onProgress } = options;
|
|
2448
|
+
const results = new Array(items.length);
|
|
2449
|
+
if (items.length === 0) {
|
|
2450
|
+
return results;
|
|
2451
|
+
}
|
|
2452
|
+
let completed = 0;
|
|
2453
|
+
let nextIndex = 0;
|
|
2454
|
+
return new Promise((resolve) => {
|
|
2455
|
+
const executing = /* @__PURE__ */ new Set();
|
|
2456
|
+
function processNext() {
|
|
2457
|
+
while (executing.size < concurrency && nextIndex < items.length) {
|
|
2458
|
+
const currentIndex = nextIndex++;
|
|
2459
|
+
const item = items[currentIndex];
|
|
2460
|
+
const promise = executor(item).then((value) => {
|
|
2461
|
+
results[currentIndex] = { success: true, value };
|
|
2462
|
+
}).catch((error) => {
|
|
2463
|
+
results[currentIndex] = {
|
|
2464
|
+
success: false,
|
|
2465
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
2466
|
+
};
|
|
2467
|
+
}).finally(() => {
|
|
2468
|
+
executing.delete(promise);
|
|
2469
|
+
completed++;
|
|
2470
|
+
onProgress?.(completed, items.length);
|
|
2471
|
+
if (completed === items.length) {
|
|
2472
|
+
resolve(results);
|
|
2473
|
+
} else {
|
|
2474
|
+
processNext();
|
|
2475
|
+
}
|
|
2476
|
+
});
|
|
2477
|
+
executing.add(promise);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
processNext();
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
// src/lib/sync/local.ts
|
|
2485
|
+
import fs4 from "fs";
|
|
2486
|
+
import path4 from "path";
|
|
2487
|
+
import { glob as glob2 } from "glob";
|
|
2488
|
+
|
|
2489
|
+
// src/lib/hash.ts
|
|
2490
|
+
import crypto3 from "crypto";
|
|
2491
|
+
function computeMD5(content) {
|
|
2492
|
+
return crypto3.createHash("md5").update(content, "utf8").digest("hex");
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
// src/lib/sync/local.ts
|
|
2496
|
+
var IGNORE_FILE_NAME = ".contextignore";
|
|
2497
|
+
var DEFAULT_INCLUDE = ["**/*.md"];
|
|
2498
|
+
var DEFAULT_EXCLUDE = [
|
|
2499
|
+
"node_modules/**",
|
|
2500
|
+
".git/**",
|
|
2501
|
+
".DS_Store",
|
|
2502
|
+
"**/.DS_Store",
|
|
2503
|
+
"**/*.tmp",
|
|
2504
|
+
"**/~*",
|
|
2505
|
+
"**/*.swp",
|
|
2506
|
+
"**/*.swo",
|
|
2507
|
+
IGNORE_FILE_NAME
|
|
2508
|
+
// Don't sync the ignore file itself
|
|
2509
|
+
];
|
|
2510
|
+
function parseIgnoreFile(content) {
|
|
2511
|
+
const patterns = [];
|
|
2512
|
+
for (const line of content.split("\n")) {
|
|
2513
|
+
const trimmed = line.trim();
|
|
2514
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
2515
|
+
continue;
|
|
2516
|
+
}
|
|
2517
|
+
patterns.push(trimmed);
|
|
2518
|
+
}
|
|
2519
|
+
return patterns;
|
|
2520
|
+
}
|
|
2521
|
+
function loadIgnorePatterns(baseDir) {
|
|
2522
|
+
const ignoreFilePath = path4.join(baseDir, IGNORE_FILE_NAME);
|
|
2523
|
+
try {
|
|
2524
|
+
if (fs4.existsSync(ignoreFilePath)) {
|
|
2525
|
+
const content = fs4.readFileSync(ignoreFilePath, "utf8");
|
|
2526
|
+
return parseIgnoreFile(content);
|
|
2527
|
+
}
|
|
2528
|
+
} catch {
|
|
2529
|
+
}
|
|
2530
|
+
return [];
|
|
2531
|
+
}
|
|
2532
|
+
async function listLocalFiles(options) {
|
|
2533
|
+
const { baseDir, include = DEFAULT_INCLUDE, exclude = [] } = options;
|
|
2534
|
+
const absoluteBaseDir = path4.resolve(baseDir);
|
|
2535
|
+
if (!fs4.existsSync(absoluteBaseDir)) {
|
|
2536
|
+
throw new Error(`Directory does not exist: ${absoluteBaseDir}`);
|
|
2537
|
+
}
|
|
2538
|
+
const stats = fs4.statSync(absoluteBaseDir);
|
|
2539
|
+
if (!stats.isDirectory()) {
|
|
2540
|
+
throw new Error(`Not a directory: ${absoluteBaseDir}`);
|
|
2541
|
+
}
|
|
2542
|
+
const ignorePatterns = loadIgnorePatterns(absoluteBaseDir);
|
|
2543
|
+
const allExclude = [...DEFAULT_EXCLUDE, ...ignorePatterns, ...exclude];
|
|
2544
|
+
const files = await glob2(include, {
|
|
2545
|
+
cwd: absoluteBaseDir,
|
|
2546
|
+
nodir: true,
|
|
2547
|
+
dot: false,
|
|
2548
|
+
ignore: allExclude
|
|
2549
|
+
});
|
|
2550
|
+
const items = /* @__PURE__ */ new Map();
|
|
2551
|
+
for (const relativePath of files) {
|
|
2552
|
+
const absolutePath = path4.join(absoluteBaseDir, relativePath);
|
|
2553
|
+
try {
|
|
2554
|
+
const fileStats = fs4.statSync(absolutePath);
|
|
2555
|
+
if (fileStats.isDirectory()) {
|
|
2556
|
+
continue;
|
|
2557
|
+
}
|
|
2558
|
+
const content = fs4.readFileSync(absolutePath, "utf8");
|
|
2559
|
+
const normalizedPath = "/" + relativePath.split(path4.sep).join("/");
|
|
2560
|
+
items.set(normalizedPath, {
|
|
2561
|
+
path: normalizedPath,
|
|
2562
|
+
absolutePath,
|
|
2563
|
+
contentHash: computeMD5(content),
|
|
2564
|
+
mtime: fileStats.mtime,
|
|
2565
|
+
size: content.length
|
|
2566
|
+
});
|
|
2567
|
+
} catch {
|
|
2568
|
+
continue;
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
return items;
|
|
2572
|
+
}
|
|
2573
|
+
function normalizePath(inputPath) {
|
|
2574
|
+
let normalized = inputPath.replace(/\\/g, "/");
|
|
2575
|
+
normalized = normalized.replace(/\/+/g, "/");
|
|
2576
|
+
if (!normalized.startsWith("/")) {
|
|
2577
|
+
normalized = "/" + normalized;
|
|
2578
|
+
}
|
|
2579
|
+
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
2580
|
+
normalized = normalized.slice(0, -1);
|
|
2581
|
+
}
|
|
2582
|
+
return normalized;
|
|
2583
|
+
}
|
|
2584
|
+
function toLocalPath(normalizedPath, baseDir) {
|
|
2585
|
+
const relativePath = normalizedPath.slice(1);
|
|
2586
|
+
return path4.join(path4.resolve(baseDir), relativePath);
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// src/lib/sync/remote.ts
|
|
2590
|
+
async function listRemoteFiles(options) {
|
|
2591
|
+
const { apiClient: apiClient2, projectId, remotePath, concurrency } = options;
|
|
2592
|
+
const allDocuments = [];
|
|
2593
|
+
let page = 1;
|
|
2594
|
+
const pageSize = 100;
|
|
2595
|
+
let hasMore = true;
|
|
2596
|
+
while (hasMore) {
|
|
2597
|
+
const response = await apiClient2.listDocuments(projectId, {
|
|
2598
|
+
page,
|
|
2599
|
+
pageSize,
|
|
2600
|
+
path: remotePath
|
|
2601
|
+
});
|
|
2602
|
+
const documents = response.documents.filter((d) => !d.isFolder);
|
|
2603
|
+
allDocuments.push(...documents);
|
|
2604
|
+
hasMore = response.pagination.page < response.pagination.totalPages;
|
|
2605
|
+
page++;
|
|
2606
|
+
}
|
|
2607
|
+
const filteredDocuments = remotePath ? allDocuments.filter((d) => {
|
|
2608
|
+
const normalizedDocPath = normalizePath(d.path);
|
|
2609
|
+
const normalizedRemotePath = normalizePath(remotePath);
|
|
2610
|
+
return normalizedDocPath === normalizedRemotePath || normalizedDocPath.startsWith(normalizedRemotePath + "/");
|
|
2611
|
+
}) : allDocuments;
|
|
2612
|
+
const results = await mapWithPool(
|
|
2613
|
+
filteredDocuments,
|
|
2614
|
+
async (doc) => {
|
|
2615
|
+
const fullDoc = await apiClient2.getDocument(projectId, doc.id);
|
|
2616
|
+
return fullDoc;
|
|
2617
|
+
},
|
|
2618
|
+
{ concurrency }
|
|
2619
|
+
);
|
|
2620
|
+
const items = /* @__PURE__ */ new Map();
|
|
2621
|
+
for (const result of results) {
|
|
2622
|
+
if (result.success) {
|
|
2623
|
+
const doc = result.value;
|
|
2624
|
+
const normalizedPath = normalizePath(doc.path);
|
|
2625
|
+
const adjustedPath = remotePath ? getRelativePath(normalizedPath, normalizePath(remotePath)) : normalizedPath;
|
|
2626
|
+
items.set(adjustedPath, {
|
|
2627
|
+
path: adjustedPath,
|
|
2628
|
+
id: doc.id,
|
|
2629
|
+
version: doc.version,
|
|
2630
|
+
contentHash: computeMD5(doc.content),
|
|
2631
|
+
mtime: new Date(doc.updatedAt),
|
|
2632
|
+
size: doc.content.length
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
return items;
|
|
2637
|
+
}
|
|
2638
|
+
function getRelativePath(fullPath, basePath) {
|
|
2639
|
+
if (fullPath === basePath) {
|
|
2640
|
+
return "/" + fullPath.split("/").pop();
|
|
2641
|
+
}
|
|
2642
|
+
if (fullPath.startsWith(basePath + "/")) {
|
|
2643
|
+
return fullPath.slice(basePath.length);
|
|
2644
|
+
}
|
|
2645
|
+
return fullPath;
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// src/lib/sync/compare.ts
|
|
2649
|
+
function compareSyncItems(localItems, remoteItems, options) {
|
|
2650
|
+
const { direction, deleteOrphans, conflictStrategy } = options;
|
|
2651
|
+
const actions = [];
|
|
2652
|
+
const conflicts = [];
|
|
2653
|
+
const allPaths = /* @__PURE__ */ new Set([...localItems.keys(), ...remoteItems.keys()]);
|
|
2654
|
+
for (const path6 of allPaths) {
|
|
2655
|
+
const local = localItems.get(path6);
|
|
2656
|
+
const remote = remoteItems.get(path6);
|
|
2657
|
+
if (local && remote) {
|
|
2658
|
+
if (local.contentHash === remote.contentHash) {
|
|
2659
|
+
actions.push({
|
|
2660
|
+
type: "skip",
|
|
2661
|
+
path: path6,
|
|
2662
|
+
local,
|
|
2663
|
+
remote,
|
|
2664
|
+
reason: "Identical content"
|
|
2665
|
+
});
|
|
2666
|
+
} else {
|
|
2667
|
+
handleContentDiff(local, remote, direction, conflictStrategy, actions, conflicts);
|
|
2668
|
+
}
|
|
2669
|
+
} else if (local && !remote) {
|
|
2670
|
+
handleLocalOnly(local, direction, deleteOrphans, actions);
|
|
2671
|
+
} else if (!local && remote) {
|
|
2672
|
+
handleRemoteOnly(remote, direction, deleteOrphans, actions);
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
actions.sort((a, b) => a.path.localeCompare(b.path));
|
|
2676
|
+
conflicts.sort((a, b) => a.path.localeCompare(b.path));
|
|
2677
|
+
return {
|
|
2678
|
+
actions,
|
|
2679
|
+
conflicts,
|
|
2680
|
+
summary: computeSummary(actions, conflicts, localItems.size, remoteItems.size)
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
function handleContentDiff(local, remote, direction, conflictStrategy, actions, conflicts) {
|
|
2684
|
+
const path6 = local.path;
|
|
2685
|
+
if (direction === "push") {
|
|
2686
|
+
actions.push({
|
|
2687
|
+
type: "update-remote",
|
|
2688
|
+
path: path6,
|
|
2689
|
+
local,
|
|
2690
|
+
remote,
|
|
2691
|
+
reason: "Local is source of truth (push mode)"
|
|
2692
|
+
});
|
|
2693
|
+
} else if (direction === "pull") {
|
|
2694
|
+
actions.push({
|
|
2695
|
+
type: "update-local",
|
|
2696
|
+
path: path6,
|
|
2697
|
+
local,
|
|
2698
|
+
remote,
|
|
2699
|
+
reason: "Remote is source of truth (pull mode)"
|
|
2700
|
+
});
|
|
2701
|
+
} else {
|
|
2702
|
+
switch (conflictStrategy) {
|
|
2703
|
+
case "newest": {
|
|
2704
|
+
const localNewer = local.mtime > remote.mtime;
|
|
2705
|
+
actions.push({
|
|
2706
|
+
type: localNewer ? "update-remote" : "update-local",
|
|
2707
|
+
path: path6,
|
|
2708
|
+
local,
|
|
2709
|
+
remote,
|
|
2710
|
+
reason: localNewer ? `Local newer (${formatTime(local.mtime)} > ${formatTime(remote.mtime)})` : `Remote newer (${formatTime(remote.mtime)} > ${formatTime(local.mtime)})`
|
|
2711
|
+
});
|
|
2712
|
+
break;
|
|
2713
|
+
}
|
|
2714
|
+
case "local":
|
|
2715
|
+
actions.push({
|
|
2716
|
+
type: "update-remote",
|
|
2717
|
+
path: path6,
|
|
2718
|
+
local,
|
|
2719
|
+
remote,
|
|
2720
|
+
reason: "Local wins (--conflict local)"
|
|
2721
|
+
});
|
|
2722
|
+
break;
|
|
2723
|
+
case "remote":
|
|
2724
|
+
actions.push({
|
|
2725
|
+
type: "update-local",
|
|
2726
|
+
path: path6,
|
|
2727
|
+
local,
|
|
2728
|
+
remote,
|
|
2729
|
+
reason: "Remote wins (--conflict remote)"
|
|
2730
|
+
});
|
|
2731
|
+
break;
|
|
2732
|
+
case "skip":
|
|
2733
|
+
actions.push({
|
|
2734
|
+
type: "skip",
|
|
2735
|
+
path: path6,
|
|
2736
|
+
local,
|
|
2737
|
+
remote,
|
|
2738
|
+
reason: "Conflict skipped (--conflict skip)"
|
|
2739
|
+
});
|
|
2740
|
+
break;
|
|
2741
|
+
case "error":
|
|
2742
|
+
default:
|
|
2743
|
+
conflicts.push({
|
|
2744
|
+
path: path6,
|
|
2745
|
+
local,
|
|
2746
|
+
remote
|
|
2747
|
+
});
|
|
2748
|
+
break;
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
function handleLocalOnly(local, direction, deleteOrphans, actions) {
|
|
2753
|
+
const path6 = local.path;
|
|
2754
|
+
if (direction === "pull") {
|
|
2755
|
+
if (deleteOrphans) {
|
|
2756
|
+
actions.push({
|
|
2757
|
+
type: "delete-local",
|
|
2758
|
+
path: path6,
|
|
2759
|
+
local,
|
|
2760
|
+
reason: "Not on remote (--delete)"
|
|
2761
|
+
});
|
|
2762
|
+
} else {
|
|
2763
|
+
actions.push({
|
|
2764
|
+
type: "skip",
|
|
2765
|
+
path: path6,
|
|
2766
|
+
local,
|
|
2767
|
+
reason: "Local-only (no --delete)"
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
} else {
|
|
2771
|
+
actions.push({
|
|
2772
|
+
type: "create-remote",
|
|
2773
|
+
path: path6,
|
|
2774
|
+
local,
|
|
2775
|
+
reason: "New local file"
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
function handleRemoteOnly(remote, direction, deleteOrphans, actions) {
|
|
2780
|
+
const path6 = remote.path;
|
|
2781
|
+
if (direction === "push") {
|
|
2782
|
+
if (deleteOrphans) {
|
|
2783
|
+
actions.push({
|
|
2784
|
+
type: "delete-remote",
|
|
2785
|
+
path: path6,
|
|
2786
|
+
remote,
|
|
2787
|
+
reason: "Not local (--delete)"
|
|
2788
|
+
});
|
|
2789
|
+
} else {
|
|
2790
|
+
actions.push({
|
|
2791
|
+
type: "skip",
|
|
2792
|
+
path: path6,
|
|
2793
|
+
remote,
|
|
2794
|
+
reason: "Remote-only (no --delete)"
|
|
2795
|
+
});
|
|
2796
|
+
}
|
|
2797
|
+
} else {
|
|
2798
|
+
actions.push({
|
|
2799
|
+
type: "create-local",
|
|
2800
|
+
path: path6,
|
|
2801
|
+
remote,
|
|
2802
|
+
reason: "New remote file"
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
function computeSummary(actions, conflicts, totalLocal, totalRemote) {
|
|
2807
|
+
const summary = {
|
|
2808
|
+
totalLocal,
|
|
2809
|
+
totalRemote,
|
|
2810
|
+
toCreate: { local: 0, remote: 0 },
|
|
2811
|
+
toUpdate: { local: 0, remote: 0 },
|
|
2812
|
+
toDelete: { local: 0, remote: 0 },
|
|
2813
|
+
toSkip: 0,
|
|
2814
|
+
conflicts: conflicts.length
|
|
2815
|
+
};
|
|
2816
|
+
for (const action of actions) {
|
|
2817
|
+
switch (action.type) {
|
|
2818
|
+
case "create-local":
|
|
2819
|
+
summary.toCreate.local++;
|
|
2820
|
+
break;
|
|
2821
|
+
case "create-remote":
|
|
2822
|
+
summary.toCreate.remote++;
|
|
2823
|
+
break;
|
|
2824
|
+
case "update-local":
|
|
2825
|
+
summary.toUpdate.local++;
|
|
2826
|
+
break;
|
|
2827
|
+
case "update-remote":
|
|
2828
|
+
summary.toUpdate.remote++;
|
|
2829
|
+
break;
|
|
2830
|
+
case "delete-local":
|
|
2831
|
+
summary.toDelete.local++;
|
|
2832
|
+
break;
|
|
2833
|
+
case "delete-remote":
|
|
2834
|
+
summary.toDelete.remote++;
|
|
2835
|
+
break;
|
|
2836
|
+
case "skip":
|
|
2837
|
+
summary.toSkip++;
|
|
2838
|
+
break;
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
return summary;
|
|
2842
|
+
}
|
|
2843
|
+
function formatTime(date) {
|
|
2844
|
+
return date.toISOString().replace("T", " ").slice(0, 19);
|
|
2845
|
+
}
|
|
2846
|
+
function hasChanges(plan) {
|
|
2847
|
+
return plan.actions.some((a) => a.type !== "skip") || plan.conflicts.length > 0;
|
|
2848
|
+
}
|
|
2849
|
+
function getOperationCount(plan) {
|
|
2850
|
+
return plan.actions.filter((a) => a.type !== "skip").length;
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
// src/lib/sync/execute.ts
|
|
2854
|
+
import fs5 from "fs";
|
|
2855
|
+
import path5 from "path";
|
|
2856
|
+
async function executeSyncPlan(plan, options) {
|
|
2857
|
+
const { apiClient: apiClient2, projectId, localBaseDir, remotePath, concurrency, onProgress } = options;
|
|
2858
|
+
const skipped = plan.actions.filter((a) => a.type === "skip");
|
|
2859
|
+
const actionsToExecute = plan.actions.filter((a) => a.type !== "skip");
|
|
2860
|
+
if (actionsToExecute.length === 0) {
|
|
2861
|
+
return { executed: [], failed: [], skipped };
|
|
2862
|
+
}
|
|
2863
|
+
const result = await executeWithPool(
|
|
2864
|
+
actionsToExecute,
|
|
2865
|
+
async (action) => {
|
|
2866
|
+
await executeSingleAction(action, {
|
|
2867
|
+
apiClient: apiClient2,
|
|
2868
|
+
projectId,
|
|
2869
|
+
localBaseDir,
|
|
2870
|
+
remotePath
|
|
2871
|
+
});
|
|
2872
|
+
},
|
|
2873
|
+
{
|
|
2874
|
+
concurrency,
|
|
2875
|
+
onProgress: (completed, total) => {
|
|
2876
|
+
const currentAction = actionsToExecute[completed - 1];
|
|
2877
|
+
onProgress?.(completed, total, currentAction);
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
);
|
|
2881
|
+
return {
|
|
2882
|
+
executed: result.successes.map((s) => s.item),
|
|
2883
|
+
failed: result.failures.map((f) => ({ action: f.item, error: f.error })),
|
|
2884
|
+
skipped
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
async function executeSingleAction(action, context2) {
|
|
2888
|
+
const { apiClient: apiClient2, projectId, localBaseDir, remotePath } = context2;
|
|
2889
|
+
switch (action.type) {
|
|
2890
|
+
case "create-local":
|
|
2891
|
+
case "update-local": {
|
|
2892
|
+
if (!action.remote) {
|
|
2893
|
+
throw new Error(`No remote info for action: ${action.path}`);
|
|
2894
|
+
}
|
|
2895
|
+
const doc = await apiClient2.getDocument(projectId, action.remote.id);
|
|
2896
|
+
const localPath = toLocalPath(action.path, localBaseDir);
|
|
2897
|
+
const dir = path5.dirname(localPath);
|
|
2898
|
+
if (!fs5.existsSync(dir)) {
|
|
2899
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
2900
|
+
}
|
|
2901
|
+
fs5.writeFileSync(localPath, doc.content, "utf8");
|
|
2902
|
+
break;
|
|
2903
|
+
}
|
|
2904
|
+
case "create-remote": {
|
|
2905
|
+
if (!action.local) {
|
|
2906
|
+
throw new Error(`No local info for action: ${action.path}`);
|
|
2907
|
+
}
|
|
2908
|
+
const content = fs5.readFileSync(action.local.absolutePath, "utf8");
|
|
2909
|
+
const title = path5.basename(action.path);
|
|
2910
|
+
const fullRemotePath = remotePath ? normalizePath(remotePath + action.path) : action.path;
|
|
2911
|
+
await apiClient2.createDocument(projectId, {
|
|
2912
|
+
title,
|
|
2913
|
+
content,
|
|
2914
|
+
path: fullRemotePath
|
|
2915
|
+
});
|
|
2916
|
+
break;
|
|
2917
|
+
}
|
|
2918
|
+
case "update-remote": {
|
|
2919
|
+
if (!action.local || !action.remote) {
|
|
2920
|
+
throw new Error(`Missing info for action: ${action.path}`);
|
|
2921
|
+
}
|
|
2922
|
+
const content = fs5.readFileSync(action.local.absolutePath, "utf8");
|
|
2923
|
+
await apiClient2.updateDocument(projectId, action.remote.id, {
|
|
2924
|
+
content,
|
|
2925
|
+
createVersion: true
|
|
2926
|
+
});
|
|
2927
|
+
break;
|
|
2928
|
+
}
|
|
2929
|
+
case "delete-local": {
|
|
2930
|
+
if (!action.local) {
|
|
2931
|
+
throw new Error(`No local info for action: ${action.path}`);
|
|
2932
|
+
}
|
|
2933
|
+
fs5.unlinkSync(action.local.absolutePath);
|
|
2934
|
+
cleanupEmptyDirs(path5.dirname(action.local.absolutePath), localBaseDir);
|
|
2935
|
+
break;
|
|
2936
|
+
}
|
|
2937
|
+
case "delete-remote": {
|
|
2938
|
+
if (!action.remote) {
|
|
2939
|
+
throw new Error(`No remote info for action: ${action.path}`);
|
|
2940
|
+
}
|
|
2941
|
+
await apiClient2.deleteDocument(projectId, action.remote.id);
|
|
2942
|
+
break;
|
|
2943
|
+
}
|
|
2944
|
+
case "skip":
|
|
2945
|
+
break;
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
function cleanupEmptyDirs(dirPath, baseDir) {
|
|
2949
|
+
const resolvedBase = path5.resolve(baseDir);
|
|
2950
|
+
let current = path5.resolve(dirPath);
|
|
2951
|
+
while (current !== resolvedBase && current.startsWith(resolvedBase)) {
|
|
2952
|
+
try {
|
|
2953
|
+
const entries = fs5.readdirSync(current);
|
|
2954
|
+
if (entries.length === 0) {
|
|
2955
|
+
fs5.rmdirSync(current);
|
|
2956
|
+
current = path5.dirname(current);
|
|
2957
|
+
} else {
|
|
2958
|
+
break;
|
|
2959
|
+
}
|
|
2960
|
+
} catch {
|
|
2961
|
+
break;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
function countOperations(result) {
|
|
2966
|
+
let uploaded = 0;
|
|
2967
|
+
let downloaded = 0;
|
|
2968
|
+
let deleted = 0;
|
|
2969
|
+
for (const action of result.executed) {
|
|
2970
|
+
switch (action.type) {
|
|
2971
|
+
case "create-remote":
|
|
2972
|
+
case "update-remote":
|
|
2973
|
+
uploaded++;
|
|
2974
|
+
break;
|
|
2975
|
+
case "create-local":
|
|
2976
|
+
case "update-local":
|
|
2977
|
+
downloaded++;
|
|
2978
|
+
break;
|
|
2979
|
+
case "delete-local":
|
|
2980
|
+
case "delete-remote":
|
|
2981
|
+
deleted++;
|
|
2982
|
+
break;
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
return {
|
|
2986
|
+
uploaded,
|
|
2987
|
+
downloaded,
|
|
2988
|
+
deleted,
|
|
2989
|
+
failed: result.failed.length
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
// src/commands/sync.ts
|
|
2994
|
+
var DEFAULT_CONCURRENCY = 5;
|
|
2995
|
+
var MAX_CONCURRENCY = 10;
|
|
2996
|
+
function collect(value, previous) {
|
|
2997
|
+
return [...previous, value];
|
|
2998
|
+
}
|
|
2999
|
+
function getProjectId4(globalOptions, context2) {
|
|
3000
|
+
const projectId = globalOptions.project || context2.config.defaultProjectId;
|
|
3001
|
+
if (!projectId) {
|
|
3002
|
+
throw new CLIError(
|
|
3003
|
+
"PROJECT_NOT_SET",
|
|
3004
|
+
"No project specified",
|
|
3005
|
+
"Use --project <id> or run 'contextloop project use <id>' to set a default"
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
3008
|
+
return projectId;
|
|
3009
|
+
}
|
|
3010
|
+
function parseConcurrency(value) {
|
|
3011
|
+
if (!value) {
|
|
3012
|
+
return DEFAULT_CONCURRENCY;
|
|
3013
|
+
}
|
|
3014
|
+
const num = parseInt(value, 10);
|
|
3015
|
+
if (isNaN(num) || num < 1) {
|
|
3016
|
+
throw new CLIError("VALIDATION_ERROR", "Concurrency must be a positive integer");
|
|
3017
|
+
}
|
|
3018
|
+
return Math.min(num, MAX_CONCURRENCY);
|
|
3019
|
+
}
|
|
3020
|
+
function parseConflictStrategy(value) {
|
|
3021
|
+
const strategies = ["newest", "local", "remote", "skip", "error"];
|
|
3022
|
+
if (!value) {
|
|
3023
|
+
return "error";
|
|
3024
|
+
}
|
|
3025
|
+
if (!strategies.includes(value)) {
|
|
3026
|
+
throw new CLIError(
|
|
3027
|
+
"VALIDATION_ERROR",
|
|
3028
|
+
`Invalid conflict strategy: ${value}`,
|
|
3029
|
+
`Valid strategies: ${strategies.join(", ")}`
|
|
3030
|
+
);
|
|
3031
|
+
}
|
|
3032
|
+
return value;
|
|
3033
|
+
}
|
|
3034
|
+
function formatDryRun(plan, globalOptions) {
|
|
3035
|
+
printOutput(
|
|
3036
|
+
plan,
|
|
3037
|
+
{
|
|
3038
|
+
text: (p) => formatDryRunText(p),
|
|
3039
|
+
json: (p) => ({
|
|
3040
|
+
dryRun: true,
|
|
3041
|
+
actions: p.actions,
|
|
3042
|
+
conflicts: p.conflicts,
|
|
3043
|
+
summary: p.summary
|
|
3044
|
+
})
|
|
3045
|
+
},
|
|
3046
|
+
globalOptions
|
|
3047
|
+
);
|
|
3048
|
+
}
|
|
3049
|
+
function formatDryRunText(plan) {
|
|
3050
|
+
const lines = [];
|
|
3051
|
+
lines.push(chalk9.cyan("\n=== Sync Preview (dry-run) ===\n"));
|
|
3052
|
+
const groups = {
|
|
3053
|
+
"create-remote": [],
|
|
3054
|
+
"update-remote": [],
|
|
3055
|
+
"create-local": [],
|
|
3056
|
+
"update-local": [],
|
|
3057
|
+
"delete-remote": [],
|
|
3058
|
+
"delete-local": []
|
|
3059
|
+
};
|
|
3060
|
+
for (const action of plan.actions) {
|
|
3061
|
+
if (action.type !== "skip" && groups[action.type]) {
|
|
3062
|
+
groups[action.type].push(action);
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
const labels = {
|
|
3066
|
+
"create-remote": { label: "Upload (new)", icon: "+", color: chalk9.green },
|
|
3067
|
+
"update-remote": { label: "Upload (update)", icon: "~", color: chalk9.yellow },
|
|
3068
|
+
"create-local": { label: "Download (new)", icon: "+", color: chalk9.green },
|
|
3069
|
+
"update-local": { label: "Download (update)", icon: "~", color: chalk9.yellow },
|
|
3070
|
+
"delete-remote": { label: "Delete remote", icon: "-", color: chalk9.red },
|
|
3071
|
+
"delete-local": { label: "Delete local", icon: "-", color: chalk9.red }
|
|
3072
|
+
};
|
|
3073
|
+
for (const [type, actions] of Object.entries(groups)) {
|
|
3074
|
+
if (actions.length > 0) {
|
|
3075
|
+
const { label, icon, color } = labels[type];
|
|
3076
|
+
lines.push(color.bold(`${label} (${actions.length}):`));
|
|
3077
|
+
for (const action of actions) {
|
|
3078
|
+
lines.push(color(` ${icon} ${action.path}`));
|
|
3079
|
+
}
|
|
3080
|
+
lines.push("");
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
if (plan.conflicts.length > 0) {
|
|
3084
|
+
lines.push(chalk9.red.bold(`Conflicts (${plan.conflicts.length}):`));
|
|
3085
|
+
for (const conflict of plan.conflicts) {
|
|
3086
|
+
lines.push(chalk9.red(` ! ${conflict.path}`));
|
|
3087
|
+
lines.push(chalk9.gray(` Local: ${conflict.local.mtime.toISOString()}`));
|
|
3088
|
+
lines.push(chalk9.gray(` Remote: ${conflict.remote.mtime.toISOString()}`));
|
|
3089
|
+
}
|
|
3090
|
+
lines.push("");
|
|
3091
|
+
}
|
|
3092
|
+
const s = plan.summary;
|
|
3093
|
+
lines.push(chalk9.bold("Summary:"));
|
|
3094
|
+
lines.push(` Upload: ${s.toCreate.remote} new, ${s.toUpdate.remote} updated`);
|
|
3095
|
+
lines.push(` Download: ${s.toCreate.local} new, ${s.toUpdate.local} updated`);
|
|
3096
|
+
lines.push(` Delete: ${s.toDelete.local} local, ${s.toDelete.remote} remote`);
|
|
3097
|
+
lines.push(` Skip: ${s.toSkip}`);
|
|
3098
|
+
if (s.conflicts > 0) {
|
|
3099
|
+
lines.push(chalk9.red(` Conflicts: ${s.conflicts}`));
|
|
3100
|
+
}
|
|
3101
|
+
lines.push("");
|
|
3102
|
+
lines.push(chalk9.cyan("Run without --dry-run to execute these changes."));
|
|
3103
|
+
return lines.join("\n");
|
|
3104
|
+
}
|
|
3105
|
+
function formatResult(result, globalOptions) {
|
|
3106
|
+
printOutput(
|
|
3107
|
+
result,
|
|
3108
|
+
{
|
|
3109
|
+
text: (r) => formatResultText(r),
|
|
3110
|
+
json: (r) => ({
|
|
3111
|
+
executed: r.executed,
|
|
3112
|
+
failed: r.failed.map((f) => ({
|
|
3113
|
+
action: f.action,
|
|
3114
|
+
error: f.error.message
|
|
3115
|
+
})),
|
|
3116
|
+
skipped: r.skipped.length
|
|
3117
|
+
})
|
|
3118
|
+
},
|
|
3119
|
+
globalOptions
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
function formatResultText(result) {
|
|
3123
|
+
const lines = [];
|
|
3124
|
+
const counts = countOperations(result);
|
|
3125
|
+
const total = counts.uploaded + counts.downloaded + counts.deleted;
|
|
3126
|
+
if (total > 0) {
|
|
3127
|
+
const parts = [];
|
|
3128
|
+
if (counts.uploaded > 0) parts.push(`${counts.uploaded} uploaded`);
|
|
3129
|
+
if (counts.downloaded > 0) parts.push(`${counts.downloaded} downloaded`);
|
|
3130
|
+
if (counts.deleted > 0) parts.push(`${counts.deleted} deleted`);
|
|
3131
|
+
lines.push(chalk9.green(`
|
|
3132
|
+
Synced ${total} file(s) (${parts.join(", ")})`));
|
|
3133
|
+
}
|
|
3134
|
+
if (result.failed.length > 0) {
|
|
3135
|
+
lines.push(chalk9.red(`
|
|
3136
|
+
Failed: ${result.failed.length} operation(s)`));
|
|
3137
|
+
for (const failure of result.failed) {
|
|
3138
|
+
lines.push(chalk9.red(` - ${failure.action.path}: ${failure.error.message}`));
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
if (total === 0 && result.failed.length === 0) {
|
|
3142
|
+
lines.push(chalk9.green("\nEverything is in sync!"));
|
|
3143
|
+
}
|
|
3144
|
+
return lines.join("\n");
|
|
3145
|
+
}
|
|
3146
|
+
async function runSync(localDir, direction, options, globalOptions, context2) {
|
|
3147
|
+
const projectId = getProjectId4(globalOptions, context2);
|
|
3148
|
+
const concurrency = parseConcurrency(options.concurrency);
|
|
3149
|
+
const conflictStrategy = parseConflictStrategy(options.conflict);
|
|
3150
|
+
const syncOptions = {
|
|
3151
|
+
direction,
|
|
3152
|
+
remotePath: options.remotePath,
|
|
3153
|
+
deleteOrphans: options.delete || false,
|
|
3154
|
+
conflictStrategy,
|
|
3155
|
+
concurrency,
|
|
3156
|
+
dryRun: options.dryRun || false,
|
|
3157
|
+
include: options.include.length > 0 ? options.include : void 0,
|
|
3158
|
+
exclude: options.exclude.length > 0 ? options.exclude : void 0
|
|
3159
|
+
};
|
|
3160
|
+
const scanSpinner = ora6("Scanning local files...").start();
|
|
3161
|
+
let localItems;
|
|
3162
|
+
try {
|
|
3163
|
+
localItems = await listLocalFiles({
|
|
3164
|
+
baseDir: localDir,
|
|
3165
|
+
include: syncOptions.include,
|
|
3166
|
+
exclude: syncOptions.exclude
|
|
3167
|
+
});
|
|
3168
|
+
scanSpinner.succeed(`Found ${localItems.size} local file(s)`);
|
|
3169
|
+
} catch (error) {
|
|
3170
|
+
scanSpinner.fail("Failed to scan local files");
|
|
3171
|
+
throw error;
|
|
3172
|
+
}
|
|
3173
|
+
const fetchSpinner = ora6("Fetching remote files...").start();
|
|
3174
|
+
let remoteItems;
|
|
3175
|
+
try {
|
|
3176
|
+
remoteItems = await listRemoteFiles({
|
|
3177
|
+
apiClient: context2.apiClient,
|
|
3178
|
+
projectId,
|
|
3179
|
+
remotePath: syncOptions.remotePath,
|
|
3180
|
+
concurrency
|
|
3181
|
+
});
|
|
3182
|
+
fetchSpinner.succeed(`Found ${remoteItems.size} remote file(s)`);
|
|
3183
|
+
} catch (error) {
|
|
3184
|
+
fetchSpinner.fail("Failed to fetch remote files");
|
|
3185
|
+
throw error;
|
|
3186
|
+
}
|
|
3187
|
+
const compareSpinner = ora6("Comparing files...").start();
|
|
3188
|
+
const plan = compareSyncItems(localItems, remoteItems, syncOptions);
|
|
3189
|
+
compareSpinner.stop();
|
|
3190
|
+
if (plan.conflicts.length > 0 && conflictStrategy === "error") {
|
|
3191
|
+
formatDryRun(plan, globalOptions);
|
|
3192
|
+
throw new CLIError(
|
|
3193
|
+
"SYNC_CONFLICT",
|
|
3194
|
+
`${plan.conflicts.length} conflict(s) detected`,
|
|
3195
|
+
"Use --conflict <strategy> to resolve (newest, local, remote, skip)"
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3198
|
+
if (syncOptions.dryRun) {
|
|
3199
|
+
formatDryRun(plan, globalOptions);
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3202
|
+
if (!hasChanges(plan)) {
|
|
3203
|
+
console.log(chalk9.green("\nEverything is in sync!"));
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
const deletionCount = plan.summary.toDelete.local + plan.summary.toDelete.remote;
|
|
3207
|
+
if (deletionCount > 0 && !options.force) {
|
|
3208
|
+
formatDryRun(plan, globalOptions);
|
|
3209
|
+
console.log(
|
|
3210
|
+
chalk9.yellow(`
|
|
3211
|
+
This will delete ${deletionCount} file(s). Use --force to skip this warning.`)
|
|
3212
|
+
);
|
|
3213
|
+
throw new CLIError("VALIDATION_ERROR", "Deletions require --force flag");
|
|
3214
|
+
}
|
|
3215
|
+
const opCount = getOperationCount(plan);
|
|
3216
|
+
const execSpinner = ora6(`Syncing 0/${opCount}...`).start();
|
|
3217
|
+
try {
|
|
3218
|
+
const result = await executeSyncPlan(plan, {
|
|
3219
|
+
apiClient: context2.apiClient,
|
|
3220
|
+
projectId,
|
|
3221
|
+
localBaseDir: localDir,
|
|
3222
|
+
remotePath: syncOptions.remotePath,
|
|
3223
|
+
concurrency,
|
|
3224
|
+
onProgress: (completed, total) => {
|
|
3225
|
+
execSpinner.text = `Syncing ${completed}/${total}...`;
|
|
3226
|
+
}
|
|
3227
|
+
});
|
|
3228
|
+
execSpinner.stop();
|
|
3229
|
+
formatResult(result, globalOptions);
|
|
3230
|
+
if (result.failed.length > 0) {
|
|
3231
|
+
throw new CLIError(
|
|
3232
|
+
"SYNC_PARTIAL_FAILURE",
|
|
3233
|
+
`${result.failed.length} operation(s) failed`,
|
|
3234
|
+
"Run again to retry failed operations"
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
} catch (error) {
|
|
3238
|
+
execSpinner.stop();
|
|
3239
|
+
throw error;
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
function registerSyncCommands(program2, context2) {
|
|
3243
|
+
const sync = program2.command("sync").description("Sync files with ContextLoop");
|
|
3244
|
+
const addCommonOptions = (cmd) => {
|
|
3245
|
+
return cmd.option("--remote-path <path>", "Remote path prefix to sync").option("--delete", "Delete files that do not exist on the source side").option(
|
|
3246
|
+
"--conflict <strategy>",
|
|
3247
|
+
"Conflict resolution: newest, local, remote, skip, error",
|
|
3248
|
+
"error"
|
|
3249
|
+
).option("--concurrency <n>", "Max concurrent operations", String(DEFAULT_CONCURRENCY)).option("--dry-run", "Preview changes without executing").option("--include <glob>", "Include pattern (can be repeated)", collect, []).option("--exclude <glob>", "Exclude pattern (can be repeated)", collect, []).option("--force", "Skip confirmation for deletions");
|
|
3250
|
+
};
|
|
3251
|
+
addCommonOptions(sync).argument("[local-dir]", "Local directory to sync", ".").action(async (localDir, options) => {
|
|
3252
|
+
const globalOptions = program2.opts();
|
|
3253
|
+
try {
|
|
3254
|
+
await runSync(localDir, "both", options, globalOptions, context2);
|
|
3255
|
+
} catch (error) {
|
|
3256
|
+
console.error(formatError(error, globalOptions.format));
|
|
3257
|
+
process.exit(getExitCode(error));
|
|
3258
|
+
}
|
|
3259
|
+
});
|
|
3260
|
+
addCommonOptions(
|
|
3261
|
+
sync.command("push").argument("<local-dir>", "Local directory to push").description("Push local files to remote (local -> remote)")
|
|
3262
|
+
).action(async (localDir, options) => {
|
|
3263
|
+
const globalOptions = program2.opts();
|
|
3264
|
+
try {
|
|
3265
|
+
await runSync(localDir, "push", options, globalOptions, context2);
|
|
3266
|
+
} catch (error) {
|
|
3267
|
+
console.error(formatError(error, globalOptions.format));
|
|
3268
|
+
process.exit(getExitCode(error));
|
|
3269
|
+
}
|
|
3270
|
+
});
|
|
3271
|
+
addCommonOptions(
|
|
3272
|
+
sync.command("pull").argument("<local-dir>", "Local directory to pull into").description("Pull remote files to local (remote -> local)")
|
|
3273
|
+
).action(async (localDir, options) => {
|
|
3274
|
+
const globalOptions = program2.opts();
|
|
3275
|
+
try {
|
|
3276
|
+
await runSync(localDir, "pull", options, globalOptions, context2);
|
|
3277
|
+
} catch (error) {
|
|
3278
|
+
console.error(formatError(error, globalOptions.format));
|
|
3279
|
+
process.exit(getExitCode(error));
|
|
3280
|
+
}
|
|
3281
|
+
});
|
|
3282
|
+
}
|
|
3283
|
+
|
|
2225
3284
|
// src/index.ts
|
|
2226
3285
|
var program = new Command();
|
|
2227
3286
|
program.name("contextloop").description("CLI for ContextLoop - manage documents and context").version("0.1.0").option("-p, --project <id>", "Project ID or slug").option("-f, --format <format>", "Output format: text, json", "text").option("-q, --quiet", "Suppress non-essential output").option("-v, --verbose", "Enable verbose logging").option("--no-color", "Disable colored output");
|
|
@@ -2238,6 +3297,7 @@ registerProjectCommands(program, context);
|
|
|
2238
3297
|
registerDocumentCommands(program, context);
|
|
2239
3298
|
registerContextCommands(program, context);
|
|
2240
3299
|
registerCommentCommands(program, context);
|
|
3300
|
+
registerSyncCommands(program, context);
|
|
2241
3301
|
async function main() {
|
|
2242
3302
|
try {
|
|
2243
3303
|
await program.parseAsync(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "contextloop-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "CLI for ContextLoop - upload, download, and manage documents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,23 +23,23 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@napi-rs/keyring": "^1.2.0",
|
|
26
|
-
"chalk": "^5.
|
|
27
|
-
"commander": "^
|
|
28
|
-
"conf": "^
|
|
29
|
-
"glob": "^
|
|
30
|
-
"ofetch": "^1.
|
|
31
|
-
"open": "^
|
|
32
|
-
"ora": "^
|
|
26
|
+
"chalk": "^5.6.2",
|
|
27
|
+
"commander": "^14.0.2",
|
|
28
|
+
"conf": "^15.0.2",
|
|
29
|
+
"glob": "^13.0.0",
|
|
30
|
+
"ofetch": "^1.5.1",
|
|
31
|
+
"open": "^11.0.0",
|
|
32
|
+
"ora": "^9.0.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@types/node": "^
|
|
36
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
37
|
-
"@typescript-eslint/parser": "^
|
|
38
|
-
"@vitest/coverage-v8": "^
|
|
39
|
-
"eslint": "^
|
|
40
|
-
"tsup": "^8.
|
|
41
|
-
"typescript": "^5.
|
|
42
|
-
"vitest": "^
|
|
35
|
+
"@types/node": "^24.10.1",
|
|
36
|
+
"@typescript-eslint/eslint-plugin": "^8.48.1",
|
|
37
|
+
"@typescript-eslint/parser": "^8.48.1",
|
|
38
|
+
"@vitest/coverage-v8": "^4.0.15",
|
|
39
|
+
"eslint": "^9.39.1",
|
|
40
|
+
"tsup": "^8.5.1",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "^4.0.15"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=20.0.0"
|