ctxman 3.0.0 → 3.1.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/.ctxman/llm-profiles.json +1067 -39
- package/.ctxman/profiles/backend-team.json +13 -0
- package/.ctxman/profiles/default.json +12 -0
- package/.ctxman/profiles/devops-team.json +21 -0
- package/.ctxman/profiles/frontend-team.json +20 -0
- package/.ctxman/templates/bug-fix.json +17 -0
- package/.ctxman/templates/code-review.json +9 -0
- package/.ctxman/templates/documentation.json +9 -0
- package/.ctxman/templates/feature.json +9 -0
- package/.ctxman/templates/full-context.json +9 -0
- package/.ctxman/templates/refactoring.json +18 -0
- package/.ctxman/templates/testing.json +9 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
- package/LICENSE +21 -0
- package/README.md +381 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -67
- package/bin/cm-audit.js +121 -0
- package/bin/cm-count.js +151 -0
- package/bin/cm-gitingest.js +128 -115
- package/bin/cm-map.js +242 -0
- package/bin/cm-mcp.js +293 -0
- package/bin/cm-pack.js +405 -0
- package/bin/cm-policy.js +170 -0
- package/bin/cm-pr.js +248 -0
- package/bin/cm-search.js +178 -0
- package/bin/cm-update.js +128 -128
- package/bin/cm-why.js +197 -0
- package/bin/mcp-server.js +8 -24
- package/ctxman.js +237 -183
- package/examples/README.md +19 -4
- package/examples/github-actions/ctxman-check.yml +49 -0
- package/examples/wizard-profiles/security-audit/profile.json +7 -1
- package/index.d.ts +1422 -0
- package/index.js +55 -31
- package/lib/agents/pr-failure-agent.js +48 -45
- package/lib/analyzers/ai-suggester.js +451 -0
- package/lib/analyzers/dependency-scanner.js +505 -0
- package/lib/analyzers/duplicate-detector.js +264 -0
- package/lib/analyzers/go-method-analyzer.js +151 -110
- package/lib/analyzers/import-tracker.js +431 -0
- package/lib/analyzers/method-analyzer.js +1055 -434
- package/lib/analyzers/performance-dashboard.js +687 -0
- package/lib/analyzers/token-calculator.js +895 -504
- package/lib/analyzers/unused-exports-detector.js +336 -0
- package/lib/api/mcp/install.js +424 -0
- package/lib/api/mcp/prompts.js +281 -281
- package/lib/api/mcp/resources.js +300 -241
- package/lib/api/mcp/sandbox.js +89 -0
- package/lib/api/mcp/server.js +772 -312
- package/lib/api/mcp/start.js +125 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
- package/lib/api/rest/server.js +105 -28
- package/lib/cache/CacheManager.js +9 -17
- package/lib/core/Analyzer.js +16 -10
- package/lib/core/ContextBuilder.js +19 -28
- package/lib/core/Reporter.js +15 -14
- package/lib/core/Scanner.js +81 -12
- package/lib/count/index.js +100 -0
- package/lib/formatters/format-registry.js +394 -372
- package/lib/formatters/gitingest-formatter.js +656 -621
- package/lib/formatters/toon-formatter-v1.3.js +637 -632
- package/lib/formatters/toon-formatter.js +239 -240
- package/lib/integrations/git/BlameTracker.js +4 -4
- package/lib/integrations/git/DiffAnalyzer.js +38 -31
- package/lib/integrations/git/GitClient.js +71 -53
- package/lib/lsp/server.js +311 -0
- package/lib/map/graph.js +299 -0
- package/lib/map/imports.js +233 -0
- package/lib/map/index.js +33 -0
- package/lib/map/rank.js +253 -0
- package/lib/map/render.js +346 -0
- package/lib/map/resolver.js +574 -0
- package/lib/pack/budget.js +111 -0
- package/lib/pack/code-lexer.js +360 -0
- package/lib/pack/compress.js +414 -0
- package/lib/pack/index.js +354 -0
- package/lib/pack/renderers/common.js +134 -0
- package/lib/pack/renderers/index.js +24 -0
- package/lib/pack/renderers/json.js +47 -0
- package/lib/pack/renderers/markdown.js +54 -0
- package/lib/pack/renderers/plain.js +38 -0
- package/lib/pack/renderers/xml.js +85 -0
- package/lib/pack/select.js +177 -0
- package/lib/pack/split.js +182 -0
- package/lib/pack/tree.js +35 -0
- package/lib/parsers/gitignore-parser.js +116 -107
- package/lib/parsers/method-filter-parser.js +50 -33
- package/lib/plugins/ExporterPlugin.js +3 -3
- package/lib/plugins/LanguagePlugin.js +3 -3
- package/lib/plugins/PluginManager.js +7 -13
- package/lib/policy/audit.js +312 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +53 -0
- package/lib/policy/policy.js +489 -0
- package/lib/pr/diff-parser.js +267 -0
- package/lib/pr/method-mapper.js +148 -0
- package/lib/pr/pr-context.js +829 -0
- package/lib/pr/pr-git.js +149 -0
- package/lib/pr/pr-renderer.js +644 -0
- package/lib/pr/related-tests.js +296 -0
- package/lib/rag/DataSourcePlugin.js +5 -5
- package/lib/rag/EmbeddingProvider.js +19 -8
- package/lib/rag/Indexer.js +14 -5
- package/lib/rag/VectorStoreFactory.js +3 -3
- package/lib/rag/ask.js +122 -0
- package/lib/rag/sources/FileDataSource.js +12 -9
- package/lib/rag/stores/BaseVectorStore.js +5 -5
- package/lib/rag/stores/ChromaDBStore.js +10 -10
- package/lib/rag/stores/LanceDBStore.js +29 -19
- package/lib/rag/stores/PineconeStore.js +11 -9
- package/lib/search/bm25.js +433 -0
- package/lib/search/code-units.js +201 -0
- package/lib/search/symbols.js +189 -0
- package/lib/ui/dashboard.js +120 -89
- package/lib/ui/index.js +1 -6
- package/lib/ui/init-wizard-ui.js +550 -0
- package/lib/ui/progress-bar.js +94 -88
- package/lib/ui/select-input.js +40 -32
- package/lib/ui/template-selector.js +99 -0
- package/lib/ui/wizard.js +374 -329
- package/lib/utils/claude-token-counter.js +151 -0
- package/lib/utils/cli-output.js +17 -0
- package/lib/utils/clipboard-utils.js +49 -50
- package/lib/utils/config-utils.js +81 -66
- package/lib/utils/context-versioning.js +532 -0
- package/lib/utils/error-handler.js +112 -112
- package/lib/utils/file-utils.js +153 -69
- package/lib/utils/format-converter.js +356 -283
- package/lib/utils/git-utils.js +366 -294
- package/lib/utils/llm-cost-estimator.js +281 -0
- package/lib/utils/llm-detector.js +242 -78
- package/lib/utils/logger.js +246 -237
- package/lib/utils/messages.js +355 -0
- package/lib/utils/multi-repo-manager.js +364 -0
- package/lib/utils/profile-manager.js +380 -0
- package/lib/utils/safe-read.js +61 -0
- package/lib/utils/secret-redactor.js +61 -0
- package/lib/utils/snapshot-manager.js +679 -0
- package/lib/utils/template-manager.js +378 -0
- package/lib/utils/token-utils.js +201 -170
- package/lib/utils/tokenizer-adapter.js +421 -377
- package/lib/utils/updater.js +539 -506
- package/lib/watch/FileWatcher.js +12 -8
- package/lib/watch/IncrementalAnalyzer.js +7 -7
- package/lib/why/coupling.js +81 -0
- package/lib/why/errors.js +23 -0
- package/lib/why/git.js +169 -0
- package/lib/why/history.js +108 -0
- package/lib/why/index.js +720 -0
- package/lib/why/ownership.js +79 -0
- package/lib/why/paths.js +207 -0
- package/lib/why/references.js +259 -0
- package/lib/why/render.js +722 -0
- package/lib/why/search.js +358 -0
- package/lib/why/target.js +389 -0
- package/lib/wizards/config-generator.js +257 -0
- package/lib/wizards/project-detector.js +572 -0
- package/package.json +44 -10
- package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
package/lib/utils/updater.js
CHANGED
|
@@ -8,558 +8,591 @@ import fs from 'fs';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import https from 'https';
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
constructor(options = {}) {
|
|
14
|
-
this.currentVersion = this.getCurrentVersion();
|
|
15
|
-
this.channel = options.channel || 'stable'; // stable or insider
|
|
16
|
-
this.autoUpdate = options.autoUpdate !== false;
|
|
17
|
-
this.checkInterval = options.checkInterval || 86400000; // 24 hours
|
|
18
|
-
this.configDir = options.configDir || path.join(process.env.HOME || process.env.USERPROFILE, '.ctxman');
|
|
19
|
-
this.updateCacheFile = path.join(this.configDir, 'update-cache.json');
|
|
20
|
-
|
|
21
|
-
// Update endpoints (placeholder)
|
|
22
|
-
this.endpoints = {
|
|
23
|
-
stable: 'https://api.github.com/repos/hakkisagdic/ctxman/releases/latest',
|
|
24
|
-
insider: 'https://api.github.com/repos/hakkisagdic/ctxman/releases', // Get all releases
|
|
25
|
-
version: 'https://raw.githubusercontent.com/hakkisagdic/ctxman/main/package.json'
|
|
26
|
-
};
|
|
27
|
-
}
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
28
14
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
15
|
+
class Updater {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.currentVersion = this.getCurrentVersion();
|
|
18
|
+
this.channel = options.channel || 'stable'; // stable or insider
|
|
19
|
+
this.autoUpdate = options.autoUpdate !== false;
|
|
20
|
+
this.checkInterval = options.checkInterval || 86400000; // 24 hours
|
|
21
|
+
this.configDir =
|
|
22
|
+
options.configDir || path.join(process.env.HOME || process.env.USERPROFILE, '.ctxman');
|
|
23
|
+
this.updateCacheFile = path.join(this.configDir, 'update-cache.json');
|
|
24
|
+
|
|
25
|
+
// Without an explicit channel, use the one saved by switchChannel()
|
|
26
|
+
if (!options.channel) {
|
|
27
|
+
const savedChannel = this.loadConfig().updateChannel;
|
|
28
|
+
if (['stable', 'insider'].includes(savedChannel)) {
|
|
29
|
+
this.channel = savedChannel;
|
|
30
|
+
}
|
|
40
31
|
}
|
|
41
32
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
resolve(JSON.parse(data));
|
|
61
|
-
} catch (error) {
|
|
62
|
-
reject(new Error('Failed to parse JSON response'));
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
}).on('error', reject);
|
|
66
|
-
});
|
|
33
|
+
// Update endpoints (placeholder)
|
|
34
|
+
this.endpoints = {
|
|
35
|
+
stable: 'https://api.github.com/repos/hakkisagdic/ctxman/releases/latest',
|
|
36
|
+
insider: 'https://api.github.com/repos/hakkisagdic/ctxman/releases', // Get all releases
|
|
37
|
+
version: 'https://raw.githubusercontent.com/hakkisagdic/ctxman/main/package.json',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get current version from package.json
|
|
43
|
+
*/
|
|
44
|
+
getCurrentVersion() {
|
|
45
|
+
try {
|
|
46
|
+
const packagePath = path.join(__dirname, '../../package.json');
|
|
47
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
48
|
+
return pkg.version;
|
|
49
|
+
} catch (_error) {
|
|
50
|
+
return '0.0.0';
|
|
67
51
|
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Fetch JSON from URL
|
|
56
|
+
*/
|
|
57
|
+
async fetchJSON(url) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
https
|
|
60
|
+
.get(
|
|
61
|
+
url,
|
|
62
|
+
{
|
|
63
|
+
headers: {
|
|
64
|
+
'User-Agent': 'ctxman-updater',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
(res) => {
|
|
68
|
+
let data = '';
|
|
69
|
+
|
|
70
|
+
res.on('data', (chunk) => {
|
|
71
|
+
data += chunk;
|
|
72
|
+
});
|
|
68
73
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
res.on('end', () => {
|
|
75
|
+
try {
|
|
76
|
+
resolve(JSON.parse(data));
|
|
77
|
+
} catch (_error) {
|
|
78
|
+
reject(new Error('Failed to parse JSON response'));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
.on('error', reject);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Compare versions (returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2)
|
|
89
|
+
*/
|
|
90
|
+
compareVersions(v1, v2) {
|
|
91
|
+
const parts1 = v1.split('.').map(Number);
|
|
92
|
+
const parts2 = v2.split('.').map(Number);
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
95
|
+
const p1 = parts1[i] || 0;
|
|
96
|
+
const p2 = parts2[i] || 0;
|
|
97
|
+
|
|
98
|
+
if (p1 < p2) return -1;
|
|
99
|
+
if (p1 > p2) return 1;
|
|
85
100
|
}
|
|
86
101
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check for updates
|
|
107
|
+
* @returns {Promise<Object>} Update information
|
|
108
|
+
*/
|
|
109
|
+
async checkForUpdates() {
|
|
110
|
+
try {
|
|
111
|
+
// PLACEHOLDER: In production, this would fetch from actual API
|
|
112
|
+
console.log('🔍 Checking for updates...');
|
|
113
|
+
console.log(` Current version: ${this.currentVersion}`);
|
|
114
|
+
console.log(` Channel: ${this.channel}`);
|
|
115
|
+
|
|
116
|
+
// Simulate API call (placeholder)
|
|
117
|
+
const latestVersion = await this.getLatestVersion();
|
|
118
|
+
|
|
119
|
+
if (!latestVersion) {
|
|
120
|
+
return {
|
|
121
|
+
available: false,
|
|
122
|
+
message: 'Could not check for updates',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const comparison = this.compareVersions(latestVersion.version, this.currentVersion);
|
|
127
|
+
|
|
128
|
+
if (comparison > 0) {
|
|
129
|
+
return {
|
|
130
|
+
available: true,
|
|
131
|
+
currentVersion: this.currentVersion,
|
|
132
|
+
latestVersion: latestVersion.version,
|
|
133
|
+
channel: this.channel,
|
|
134
|
+
releaseNotes: latestVersion.releaseNotes || '',
|
|
135
|
+
downloadUrl: latestVersion.downloadUrl || '',
|
|
136
|
+
publishedAt: latestVersion.publishedAt || new Date().toISOString(),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
available: false,
|
|
142
|
+
currentVersion: this.currentVersion,
|
|
143
|
+
latestVersion: latestVersion.version,
|
|
144
|
+
message: 'You are using the latest version',
|
|
145
|
+
};
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return {
|
|
148
|
+
available: false,
|
|
149
|
+
error: error.message,
|
|
150
|
+
message: 'Failed to check for updates',
|
|
151
|
+
};
|
|
135
152
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// Fallback to latest stable if no pre-release
|
|
171
|
-
const latestRelease = releases[0];
|
|
172
|
-
return {
|
|
173
|
-
version: latestRelease.tag_name.replace(/^v/, ''),
|
|
174
|
-
releaseNotes: latestRelease.body || 'No release notes available',
|
|
175
|
-
downloadUrl: latestRelease.html_url,
|
|
176
|
-
publishedAt: latestRelease.published_at,
|
|
177
|
-
assets: latestRelease.assets || []
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
} catch (error) {
|
|
181
|
-
console.error('Failed to fetch latest version:', error.message);
|
|
182
|
-
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get latest version info from GitHub API
|
|
157
|
+
*/
|
|
158
|
+
async getLatestVersion() {
|
|
159
|
+
try {
|
|
160
|
+
if (this.channel === 'stable') {
|
|
161
|
+
// Stable channel - get latest stable release
|
|
162
|
+
const release = await this.fetchJSON(this.endpoints.stable);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
version: release.tag_name.replace(/^v/, ''), // Remove 'v' prefix
|
|
166
|
+
releaseNotes: release.body || 'No release notes available',
|
|
167
|
+
downloadUrl: release.html_url,
|
|
168
|
+
publishedAt: release.published_at,
|
|
169
|
+
assets: release.assets || [],
|
|
170
|
+
};
|
|
171
|
+
} else {
|
|
172
|
+
// Insider channel - get latest pre-release
|
|
173
|
+
const releases = await this.fetchJSON(this.endpoints.insider);
|
|
174
|
+
|
|
175
|
+
// Find latest pre-release
|
|
176
|
+
const preRelease = releases.find((r) => r.prerelease === true);
|
|
177
|
+
|
|
178
|
+
if (preRelease) {
|
|
179
|
+
return {
|
|
180
|
+
version: preRelease.tag_name.replace(/^v/, ''),
|
|
181
|
+
releaseNotes: preRelease.body || 'No release notes available',
|
|
182
|
+
downloadUrl: preRelease.html_url,
|
|
183
|
+
publishedAt: preRelease.published_at,
|
|
184
|
+
assets: preRelease.assets || [],
|
|
185
|
+
};
|
|
183
186
|
}
|
|
184
|
-
}
|
|
185
187
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
console.log(` Installation type: ${installType}`);
|
|
200
|
-
|
|
201
|
-
// Step 2: Create backup
|
|
202
|
-
console.log(' Creating backup...');
|
|
203
|
-
const backupPath = this.createBackup();
|
|
204
|
-
console.log(` ✓ Backup created: ${backupPath}`);
|
|
205
|
-
|
|
206
|
-
// Step 3: Install based on type
|
|
207
|
-
if (installType === 'global') {
|
|
208
|
-
console.log(' Updating global installation...');
|
|
209
|
-
const result = execSync('npm update -g ctxman', {
|
|
210
|
-
encoding: 'utf8',
|
|
211
|
-
stdio: 'pipe'
|
|
212
|
-
});
|
|
213
|
-
console.log(' ✓ Global update complete');
|
|
214
|
-
} else if (installType === 'local') {
|
|
215
|
-
console.log(' Updating local installation...');
|
|
216
|
-
const result = execSync('npm update ctxman', {
|
|
217
|
-
encoding: 'utf8',
|
|
218
|
-
stdio: 'pipe',
|
|
219
|
-
cwd: this.getInstallDir()
|
|
220
|
-
});
|
|
221
|
-
console.log(' ✓ Local update complete');
|
|
222
|
-
} else {
|
|
223
|
-
// Source installation - show manual instructions
|
|
224
|
-
console.log('\n⚠️ Source-based installation detected.');
|
|
225
|
-
console.log(' Please update manually:\n');
|
|
226
|
-
console.log(' 1. git pull origin main');
|
|
227
|
-
console.log(' 2. npm install');
|
|
228
|
-
console.log(` 3. Or visit: ${updateInfo.downloadUrl}\n`);
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
success: false,
|
|
232
|
-
manualUpdateRequired: true,
|
|
233
|
-
instructions: 'Manual update required for source installation'
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Step 4: Verify installation
|
|
238
|
-
console.log(' Verifying update...');
|
|
239
|
-
const newVersion = this.getCurrentVersion();
|
|
240
|
-
|
|
241
|
-
if (this.compareVersions(newVersion, updateInfo.latestVersion) === 0) {
|
|
242
|
-
console.log(' ✓ Update verified');
|
|
243
|
-
|
|
244
|
-
// Update config
|
|
245
|
-
this.saveConfig({
|
|
246
|
-
channel: this.channel,
|
|
247
|
-
autoUpdate: this.autoUpdate
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
return {
|
|
251
|
-
success: true,
|
|
252
|
-
oldVersion: updateInfo.currentVersion,
|
|
253
|
-
newVersion: newVersion,
|
|
254
|
-
backupPath
|
|
255
|
-
};
|
|
256
|
-
} else {
|
|
257
|
-
console.log(' ⚠️ Version mismatch after update');
|
|
258
|
-
console.log(` Expected: ${updateInfo.latestVersion}, Got: ${newVersion}`);
|
|
259
|
-
|
|
260
|
-
return {
|
|
261
|
-
success: false,
|
|
262
|
-
error: 'Version verification failed'
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
} catch (error) {
|
|
266
|
-
console.error(' ✗ Update failed:', error.message);
|
|
267
|
-
|
|
268
|
-
return {
|
|
269
|
-
success: false,
|
|
270
|
-
error: error.message
|
|
271
|
-
};
|
|
272
|
-
}
|
|
188
|
+
// Fallback to latest stable if no pre-release
|
|
189
|
+
const latestRelease = releases[0];
|
|
190
|
+
return {
|
|
191
|
+
version: latestRelease.tag_name.replace(/^v/, ''),
|
|
192
|
+
releaseNotes: latestRelease.body || 'No release notes available',
|
|
193
|
+
downloadUrl: latestRelease.html_url,
|
|
194
|
+
publishedAt: latestRelease.published_at,
|
|
195
|
+
assets: latestRelease.assets || [],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('Failed to fetch latest version:', error.message);
|
|
200
|
+
return null;
|
|
273
201
|
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Download and install update
|
|
206
|
+
*/
|
|
207
|
+
async installUpdate(updateInfo) {
|
|
208
|
+
try {
|
|
209
|
+
console.log('📦 Installing update...');
|
|
210
|
+
console.log(` From: ${updateInfo.currentVersion}`);
|
|
211
|
+
console.log(` To: ${updateInfo.latestVersion}`);
|
|
212
|
+
console.log();
|
|
213
|
+
|
|
214
|
+
// Step 1: Detect installation type
|
|
215
|
+
const installType = this.detectInstallationType();
|
|
216
|
+
console.log(` Installation type: ${installType}`);
|
|
217
|
+
|
|
218
|
+
// Step 2: Create backup
|
|
219
|
+
console.log(' Creating backup...');
|
|
220
|
+
const backupPath = this.createBackup();
|
|
221
|
+
console.log(` ✓ Backup created: ${backupPath}`);
|
|
222
|
+
|
|
223
|
+
// Step 3: Install based on type
|
|
224
|
+
if (installType === 'global') {
|
|
225
|
+
console.log(' Updating global installation...');
|
|
226
|
+
const _result = execSync('npm update -g ctxman', {
|
|
227
|
+
encoding: 'utf8',
|
|
228
|
+
stdio: 'pipe',
|
|
229
|
+
});
|
|
230
|
+
console.log(' ✓ Global update complete');
|
|
231
|
+
} else if (installType === 'local') {
|
|
232
|
+
console.log(' Updating local installation...');
|
|
233
|
+
const _result = execSync('npm update ctxman', {
|
|
234
|
+
encoding: 'utf8',
|
|
235
|
+
stdio: 'pipe',
|
|
236
|
+
cwd: this.getInstallDir(),
|
|
237
|
+
});
|
|
238
|
+
console.log(' ✓ Local update complete');
|
|
239
|
+
} else {
|
|
240
|
+
// Source installation - show manual instructions
|
|
241
|
+
console.log('\n⚠️ Source-based installation detected.');
|
|
242
|
+
console.log(' Please update manually:\n');
|
|
243
|
+
console.log(' 1. git pull origin main');
|
|
244
|
+
console.log(' 2. npm install');
|
|
245
|
+
console.log(` 3. Or visit: ${updateInfo.downloadUrl}\n`);
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
manualUpdateRequired: true,
|
|
250
|
+
instructions: 'Manual update required for source installation',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
274
253
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
detectInstallationType() {
|
|
279
|
-
try {
|
|
280
|
-
// Check if installed globally
|
|
281
|
-
const globalList = execSync('npm list -g ctxman --depth=0', {
|
|
282
|
-
encoding: 'utf8',
|
|
283
|
-
stdio: 'pipe'
|
|
284
|
-
});
|
|
254
|
+
// Step 4: Verify installation
|
|
255
|
+
console.log(' Verifying update...');
|
|
256
|
+
const newVersion = this.getCurrentVersion();
|
|
285
257
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
} catch (error) {
|
|
290
|
-
// Not global
|
|
291
|
-
}
|
|
258
|
+
if (this.compareVersions(newVersion, updateInfo.latestVersion) === 0) {
|
|
259
|
+
console.log(' ✓ Update verified');
|
|
292
260
|
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
261
|
+
// Update config
|
|
262
|
+
this.saveConfig({
|
|
263
|
+
channel: this.channel,
|
|
264
|
+
autoUpdate: this.autoUpdate,
|
|
265
|
+
});
|
|
298
266
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
267
|
+
return {
|
|
268
|
+
success: true,
|
|
269
|
+
oldVersion: updateInfo.currentVersion,
|
|
270
|
+
newVersion: newVersion,
|
|
271
|
+
backupPath,
|
|
272
|
+
};
|
|
273
|
+
} else {
|
|
274
|
+
console.log(' ⚠️ Version mismatch after update');
|
|
275
|
+
console.log(` Expected: ${updateInfo.latestVersion}, Got: ${newVersion}`);
|
|
304
276
|
|
|
305
|
-
return
|
|
277
|
+
return {
|
|
278
|
+
success: false,
|
|
279
|
+
error: 'Version verification failed',
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.error(' ✗ Update failed:', error.message);
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
success: false,
|
|
287
|
+
error: error.message,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Detect installation type (global, local, or source)
|
|
294
|
+
*/
|
|
295
|
+
detectInstallationType() {
|
|
296
|
+
try {
|
|
297
|
+
// Check if installed globally
|
|
298
|
+
const globalList = execSync('npm list -g ctxman --depth=0', {
|
|
299
|
+
encoding: 'utf8',
|
|
300
|
+
stdio: 'pipe',
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (globalList.includes('ctxman')) {
|
|
304
|
+
return 'global';
|
|
305
|
+
}
|
|
306
|
+
} catch (_error) {
|
|
307
|
+
// Not global
|
|
306
308
|
}
|
|
307
309
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (installType === 'global') {
|
|
315
|
-
try {
|
|
316
|
-
const globalRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
|
|
317
|
-
return path.join(globalRoot, 'ctxman');
|
|
318
|
-
} catch (error) {
|
|
319
|
-
return null;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (installType === 'local') {
|
|
324
|
-
return path.join(process.cwd(), 'node_modules', '@hakkisagdic', 'ctxman');
|
|
325
|
-
}
|
|
310
|
+
// Check if in node_modules (local npm install)
|
|
311
|
+
const nodeModulesPath = path.join(process.cwd(), 'node_modules', '@hakkisagdic', 'ctxman');
|
|
312
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
313
|
+
return 'local';
|
|
314
|
+
}
|
|
326
315
|
|
|
327
|
-
|
|
316
|
+
// Check if running from source (has .git directory)
|
|
317
|
+
const gitPath = path.join(__dirname, '../../.git');
|
|
318
|
+
if (fs.existsSync(gitPath)) {
|
|
319
|
+
return 'source';
|
|
328
320
|
}
|
|
329
321
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
*/
|
|
333
|
-
createBackup() {
|
|
322
|
+
return 'unknown';
|
|
323
|
+
}
|
|
334
324
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
325
|
+
/**
|
|
326
|
+
* Get installation directory
|
|
327
|
+
*/
|
|
328
|
+
getInstallDir() {
|
|
329
|
+
const installType = this.detectInstallationType();
|
|
339
330
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
331
|
+
if (installType === 'global') {
|
|
332
|
+
try {
|
|
333
|
+
const globalRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
|
|
334
|
+
return path.join(globalRoot, 'ctxman');
|
|
335
|
+
} catch (_error) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
343
339
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
timestamp: new Date().toISOString(),
|
|
348
|
-
installType: this.detectInstallationType(),
|
|
349
|
-
installDir: installDir
|
|
350
|
-
};
|
|
340
|
+
if (installType === 'local') {
|
|
341
|
+
return path.join(process.cwd(), 'node_modules', '@hakkisagdic', 'ctxman');
|
|
342
|
+
}
|
|
351
343
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
JSON.stringify(backupInfo, null, 2),
|
|
355
|
-
'utf8'
|
|
356
|
-
);
|
|
344
|
+
return path.join(__dirname, '../..');
|
|
345
|
+
}
|
|
357
346
|
|
|
358
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Create backup before update
|
|
349
|
+
*/
|
|
350
|
+
createBackup() {
|
|
351
|
+
const installDir = this.getInstallDir();
|
|
352
|
+
const backupDir = path.join(this.configDir, 'backups');
|
|
353
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
354
|
+
const backupPath = path.join(backupDir, `backup-${this.currentVersion}-${timestamp}`);
|
|
355
|
+
|
|
356
|
+
if (!fs.existsSync(backupDir)) {
|
|
357
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
359
358
|
}
|
|
360
359
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
360
|
+
// Create backup info file
|
|
361
|
+
const backupInfo = {
|
|
362
|
+
version: this.currentVersion,
|
|
363
|
+
timestamp: new Date().toISOString(),
|
|
364
|
+
installType: this.detectInstallationType(),
|
|
365
|
+
installDir: installDir,
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
fs.writeFileSync(
|
|
369
|
+
path.join(backupPath, 'backup-info.json'),
|
|
370
|
+
JSON.stringify(backupInfo, null, 2),
|
|
371
|
+
'utf8'
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
return backupPath;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Rollback to previous version
|
|
379
|
+
*/
|
|
380
|
+
async rollback() {
|
|
381
|
+
try {
|
|
382
|
+
console.log('🔄 Rolling back to previous version...');
|
|
383
|
+
|
|
384
|
+
const backupDir = path.join(this.configDir, 'backups');
|
|
385
|
+
if (!fs.existsSync(backupDir)) {
|
|
386
|
+
throw new Error('No backups found');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Get latest backup
|
|
390
|
+
const backups = fs
|
|
391
|
+
.readdirSync(backupDir)
|
|
392
|
+
.filter((name) => name.startsWith('backup-'))
|
|
393
|
+
.sort()
|
|
394
|
+
.reverse();
|
|
395
|
+
|
|
396
|
+
if (backups.length === 0) {
|
|
397
|
+
throw new Error('No backups available');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const latestBackup = backups[0];
|
|
401
|
+
const backupPath = path.join(backupDir, latestBackup);
|
|
402
|
+
const backupInfoPath = path.join(backupPath, 'backup-info.json');
|
|
403
|
+
|
|
404
|
+
if (!fs.existsSync(backupInfoPath)) {
|
|
405
|
+
throw new Error('Invalid backup: missing backup-info.json');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const backupInfo = JSON.parse(fs.readFileSync(backupInfoPath, 'utf8'));
|
|
409
|
+
|
|
410
|
+
console.log(` Restoring version: ${backupInfo.version}`);
|
|
411
|
+
console.log(` Backup date: ${backupInfo.timestamp}`);
|
|
412
|
+
|
|
413
|
+
// Install the backed up version
|
|
414
|
+
const installType = backupInfo.installType;
|
|
415
|
+
|
|
416
|
+
if (installType === 'global') {
|
|
417
|
+
execSync(`npm install -g ctxman@${backupInfo.version}`, {
|
|
418
|
+
stdio: 'inherit',
|
|
419
|
+
});
|
|
420
|
+
} else if (installType === 'local') {
|
|
421
|
+
execSync(`npm install ctxman@${backupInfo.version}`, {
|
|
422
|
+
stdio: 'inherit',
|
|
423
|
+
cwd: this.getInstallDir(),
|
|
424
|
+
});
|
|
425
|
+
} else {
|
|
426
|
+
throw new Error('Cannot rollback source installation');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
console.log('\n✅ Rollback successful!');
|
|
430
|
+
console.log(` Restored to version: ${backupInfo.version}`);
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
success: true,
|
|
434
|
+
restoredVersion: backupInfo.version,
|
|
435
|
+
};
|
|
436
|
+
} catch (error) {
|
|
437
|
+
console.error('✗ Rollback failed:', error.message);
|
|
438
|
+
return {
|
|
439
|
+
success: false,
|
|
440
|
+
error: error.message,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Switch update channel
|
|
447
|
+
*/
|
|
448
|
+
async switchChannel(newChannel) {
|
|
449
|
+
if (!['stable', 'insider'].includes(newChannel)) {
|
|
450
|
+
throw new Error('Invalid channel. Must be "stable" or "insider"');
|
|
427
451
|
}
|
|
428
452
|
|
|
429
|
-
|
|
430
|
-
* Switch update channel
|
|
431
|
-
*/
|
|
432
|
-
async switchChannel(newChannel) {
|
|
433
|
-
if (!['stable', 'insider'].includes(newChannel)) {
|
|
434
|
-
throw new Error('Invalid channel. Must be "stable" or "insider"');
|
|
435
|
-
}
|
|
453
|
+
this.channel = newChannel;
|
|
436
454
|
|
|
437
|
-
|
|
455
|
+
// Save to config
|
|
456
|
+
this.saveConfig({ channel: newChannel });
|
|
438
457
|
|
|
439
|
-
|
|
440
|
-
|
|
458
|
+
console.log(`✓ Switched to ${newChannel} channel`);
|
|
459
|
+
console.log(' Checking for updates on new channel...\n');
|
|
441
460
|
|
|
442
|
-
|
|
443
|
-
|
|
461
|
+
return await this.checkForUpdates();
|
|
462
|
+
}
|
|
444
463
|
|
|
445
|
-
|
|
464
|
+
/**
|
|
465
|
+
* Load updater config written by saveConfig()
|
|
466
|
+
*/
|
|
467
|
+
loadConfig() {
|
|
468
|
+
try {
|
|
469
|
+
const configFile = path.join(this.configDir, 'config.json');
|
|
470
|
+
if (fs.existsSync(configFile)) {
|
|
471
|
+
return JSON.parse(fs.readFileSync(configFile, 'utf8')) || {};
|
|
472
|
+
}
|
|
473
|
+
} catch (_error) {
|
|
474
|
+
// An unreadable config falls back to the defaults
|
|
446
475
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
476
|
+
return {};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Save updater config
|
|
481
|
+
*/
|
|
482
|
+
saveConfig(config) {
|
|
483
|
+
try {
|
|
484
|
+
const configFile = path.join(this.configDir, 'config.json');
|
|
485
|
+
let existingConfig = {};
|
|
486
|
+
try {
|
|
487
|
+
existingConfig = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
488
|
+
} catch (error) {
|
|
489
|
+
// No config yet; an unparsable one stops the save below
|
|
490
|
+
if (error.code !== 'ENOENT') throw error;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const updatedConfig = {
|
|
494
|
+
...existingConfig,
|
|
495
|
+
updateChannel: config.channel || this.channel,
|
|
496
|
+
autoUpdate: config.autoUpdate !== undefined ? config.autoUpdate : this.autoUpdate,
|
|
497
|
+
lastUpdateCheck: new Date().toISOString(),
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
if (!fs.existsSync(this.configDir)) {
|
|
501
|
+
fs.mkdirSync(this.configDir, { recursive: true });
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
fs.writeFileSync(configFile, JSON.stringify(updatedConfig, null, 2), 'utf8');
|
|
505
|
+
} catch (error) {
|
|
506
|
+
console.error('Failed to save config:', error.message);
|
|
475
507
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Get update cache
|
|
512
|
+
*/
|
|
513
|
+
getUpdateCache() {
|
|
514
|
+
try {
|
|
515
|
+
if (fs.existsSync(this.updateCacheFile)) {
|
|
516
|
+
return JSON.parse(fs.readFileSync(this.updateCacheFile, 'utf8'));
|
|
517
|
+
}
|
|
518
|
+
} catch (_error) {
|
|
519
|
+
// Return null if cache can't be read
|
|
489
520
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Save update cache
|
|
526
|
+
*/
|
|
527
|
+
saveUpdateCache(data) {
|
|
528
|
+
try {
|
|
529
|
+
if (!fs.existsSync(this.configDir)) {
|
|
530
|
+
fs.mkdirSync(this.configDir, { recursive: true });
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
fs.writeFileSync(this.updateCacheFile, JSON.stringify(data, null, 2), 'utf8');
|
|
534
|
+
} catch (_error) {
|
|
535
|
+
// Fail silently
|
|
504
536
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const lastCheck = new Date(cache.lastCheck).getTime();
|
|
516
|
-
const now = Date.now();
|
|
517
|
-
|
|
518
|
-
return (now - lastCheck) > this.checkInterval;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Should check for updates (based on interval)
|
|
541
|
+
*/
|
|
542
|
+
shouldCheckForUpdates() {
|
|
543
|
+
const cache = this.getUpdateCache();
|
|
544
|
+
if (!cache || !cache.lastCheck) {
|
|
545
|
+
return true;
|
|
519
546
|
}
|
|
520
547
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
548
|
+
const lastCheck = new Date(cache.lastCheck).getTime();
|
|
549
|
+
const now = Date.now();
|
|
550
|
+
|
|
551
|
+
return now - lastCheck > this.checkInterval;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Check and notify about updates
|
|
556
|
+
*/
|
|
557
|
+
async checkAndNotify() {
|
|
558
|
+
if (!this.shouldCheckForUpdates()) {
|
|
559
|
+
const cache = this.getUpdateCache();
|
|
560
|
+
if (cache && cache.available) {
|
|
561
|
+
this.showUpdateNotification(cache);
|
|
562
|
+
}
|
|
563
|
+
return cache;
|
|
564
|
+
}
|
|
532
565
|
|
|
533
|
-
|
|
566
|
+
const updateInfo = await this.checkForUpdates();
|
|
534
567
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
568
|
+
// Save to cache
|
|
569
|
+
this.saveUpdateCache({
|
|
570
|
+
...updateInfo,
|
|
571
|
+
lastCheck: new Date().toISOString(),
|
|
572
|
+
});
|
|
540
573
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
return updateInfo;
|
|
574
|
+
if (updateInfo.available) {
|
|
575
|
+
this.showUpdateNotification(updateInfo);
|
|
546
576
|
}
|
|
547
577
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
578
|
+
return updateInfo;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Show update notification
|
|
583
|
+
*/
|
|
584
|
+
showUpdateNotification(updateInfo) {
|
|
585
|
+
console.log('\n╔════════════════════════════════════════════════════════╗');
|
|
586
|
+
console.log('║ 🎉 Update Available! ║');
|
|
587
|
+
console.log('╚════════════════════════════════════════════════════════╝\n');
|
|
588
|
+
console.log(` Current version: ${updateInfo.currentVersion}`);
|
|
589
|
+
console.log(` Latest version: ${updateInfo.latestVersion}`);
|
|
590
|
+
console.log(` Channel: ${updateInfo.channel}\n`);
|
|
591
|
+
console.log(' To update, run:');
|
|
592
|
+
console.log(' ctxman update\n');
|
|
593
|
+
console.log(' Or update manually:');
|
|
594
|
+
console.log(' npm update -g ctxman\n');
|
|
595
|
+
}
|
|
563
596
|
}
|
|
564
597
|
|
|
565
598
|
export default Updater;
|