soso-ppm 2.4.8

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.
@@ -0,0 +1,143 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const semver = require('semver');
6
+ const { loadRegistry, saveRegistry } = require('../config');
7
+ const { success, error, info, warn } = require('../utils/logger');
8
+ const git = require('../utils/git');
9
+
10
+ /**
11
+ * Publish package to registry
12
+ */
13
+ async function publish(args) {
14
+ const cwd = process.cwd();
15
+ const packageJsonPath = path.join(cwd, 'package.json');
16
+
17
+ // Load package.json
18
+ if (!fs.existsSync(packageJsonPath)) {
19
+ throw new Error('No package.json found in current directory');
20
+ }
21
+
22
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
23
+
24
+ // Validate package.json
25
+ validatePackageJson(packageJson);
26
+
27
+ const { name, version } = packageJson;
28
+
29
+ info(`Publishing ${name}@${version}...`);
30
+
31
+ // Check if Git repository
32
+ const gitDir = path.join(cwd, '.git');
33
+ if (!fs.existsSync(gitDir)) {
34
+ throw new Error('Not a git repository. Initialize with: git init');
35
+ }
36
+
37
+ // Check if working directory is clean
38
+ const isClean = await git.isClean(cwd);
39
+ if (!isClean) {
40
+ throw new Error('Working directory is not clean. Commit or stash changes before publishing.');
41
+ }
42
+
43
+ // Get git remote URL
44
+ const remoteUrl = await getGitRemoteUrl(cwd);
45
+ if (!remoteUrl) {
46
+ throw new Error('No git remote configured. Add remote with: git remote add origin <url>');
47
+ }
48
+
49
+ info(`Git remote: ${remoteUrl}`);
50
+
51
+ // Load registry
52
+ const registry = loadRegistry();
53
+
54
+ // Check if package exists
55
+ if (!registry.packages[name]) {
56
+ registry.packages[name] = {
57
+ name,
58
+ versions: {}
59
+ };
60
+ info(`Adding new package: ${name}`);
61
+ }
62
+
63
+ // Check if version already exists
64
+ if (registry.packages[name].versions[version]) {
65
+ throw new Error(`Version ${version} already published. Update version in package.json.`);
66
+ }
67
+
68
+ // Create git tag
69
+ const tag = `v${version}`;
70
+ info(`Creating tag: ${tag}`);
71
+
72
+ try {
73
+ await git.createTag(tag, `Release ${version}`, cwd);
74
+ } catch (err) {
75
+ throw new Error(`Failed to create tag: ${err.message}`);
76
+ }
77
+
78
+ // Push tag to remote
79
+ info('Pushing tag to remote...');
80
+ try {
81
+ await git.pushTags(cwd);
82
+ } catch (err) {
83
+ // Rollback: delete local tag
84
+ await git.execGit(['tag', '-d', tag], { cwd });
85
+ throw new Error(`Failed to push tag: ${err.message}`);
86
+ }
87
+
88
+ // Update registry
89
+ registry.packages[name].versions[version] = {
90
+ version,
91
+ gitUrl: remoteUrl,
92
+ tag,
93
+ dependencies: packageJson.dependencies || {},
94
+ publishedAt: new Date().toISOString()
95
+ };
96
+
97
+ saveRegistry(registry);
98
+
99
+ success(`Published ${name}@${version}`);
100
+ info(`Package available at: ${remoteUrl}#${tag}`);
101
+ }
102
+
103
+ /**
104
+ * Validate package.json
105
+ */
106
+ function validatePackageJson(packageJson) {
107
+ // Check required fields
108
+ if (!packageJson.name) {
109
+ throw new Error('package.json missing required field: name');
110
+ }
111
+
112
+ if (!packageJson.version) {
113
+ throw new Error('package.json missing required field: version');
114
+ }
115
+
116
+ // Validate name
117
+ const nameRegex = /^(@[a-z0-9-]+\/)?[a-z0-9-]+$/;
118
+ if (!nameRegex.test(packageJson.name)) {
119
+ throw new Error(
120
+ 'Invalid package name. Use lowercase letters, numbers, and hyphens. ' +
121
+ 'Scoped packages: @scope/name'
122
+ );
123
+ }
124
+
125
+ // Validate version
126
+ if (!semver.valid(packageJson.version)) {
127
+ throw new Error(`Invalid version: ${packageJson.version}. Must follow semver (e.g., 1.2.3)`);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Get git remote URL
133
+ */
134
+ async function getGitRemoteUrl(cwd) {
135
+ try {
136
+ const output = await git.execGit(['remote', 'get-url', 'origin'], { cwd });
137
+ return output.trim();
138
+ } catch (error) {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ module.exports = { publish };
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const semver = require('semver');
6
+ const { loadRegistry } = require('../config');
7
+ const { install } = require('./install');
8
+ const { success, info, warn } = require('../utils/logger');
9
+
10
+ /**
11
+ * Update dependencies
12
+ */
13
+ async function update(args) {
14
+ const cwd = process.cwd();
15
+ const packageJsonPath = path.join(cwd, 'package.json');
16
+
17
+ // Load package.json
18
+ if (!fs.existsSync(packageJsonPath)) {
19
+ throw new Error('No package.json found in current directory');
20
+ }
21
+
22
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
23
+ const dependencies = packageJson.dependencies || {};
24
+
25
+ if (Object.keys(dependencies).length === 0) {
26
+ info('No dependencies to update');
27
+ return;
28
+ }
29
+
30
+ // Determine what to update
31
+ let specificPackage = null;
32
+ if (args.length > 0 && !args[0].startsWith('-')) {
33
+ specificPackage = args[0];
34
+ }
35
+
36
+ const registry = loadRegistry();
37
+ let updated = false;
38
+
39
+ if (specificPackage) {
40
+ // Update specific package
41
+ if (!dependencies[specificPackage]) {
42
+ throw new Error(`Package ${specificPackage} not found in dependencies`);
43
+ }
44
+
45
+ info(`Checking for updates to ${specificPackage}...`);
46
+ const newVersion = await findLatestVersion(specificPackage, dependencies[specificPackage], registry);
47
+
48
+ if (newVersion) {
49
+ dependencies[specificPackage] = `^${newVersion}`;
50
+ info(`Updated ${specificPackage} to ^${newVersion}`);
51
+ updated = true;
52
+ } else {
53
+ info(`${specificPackage} is already at latest version`);
54
+ }
55
+ } else {
56
+ // Update all packages
57
+ info('Checking for updates...');
58
+
59
+ for (const [name, range] of Object.entries(dependencies)) {
60
+ const newVersion = await findLatestVersion(name, range, registry);
61
+
62
+ if (newVersion) {
63
+ dependencies[name] = `^${newVersion}`;
64
+ info(`Updated ${name} to ^${newVersion}`);
65
+ updated = true;
66
+ }
67
+ }
68
+ }
69
+
70
+ if (updated) {
71
+ // Save updated package.json
72
+ packageJson.dependencies = dependencies;
73
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
74
+
75
+ // Reinstall
76
+ info('Reinstalling dependencies...');
77
+ await install([]);
78
+
79
+ success('Dependencies updated');
80
+ } else {
81
+ success('All dependencies are up to date');
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Find latest version satisfying range
87
+ */
88
+ async function findLatestVersion(name, range, registry) {
89
+ const packageData = registry.packages[name];
90
+ if (!packageData) {
91
+ warn(`Package ${name} not found in registry`);
92
+ return null;
93
+ }
94
+
95
+ const versions = Object.keys(packageData.versions).sort(semver.rcompare);
96
+
97
+ // Find latest version matching range
98
+ const currentVersion = semver.maxSatisfying(versions, range);
99
+
100
+ // Find absolute latest version
101
+ const latestVersion = versions[0];
102
+
103
+ // Only update if there's a newer version
104
+ if (currentVersion && semver.gt(latestVersion, currentVersion)) {
105
+ return latestVersion;
106
+ }
107
+
108
+ return null;
109
+ }
110
+
111
+ module.exports = { update };
package/lib/config.js ADDED
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ /**
8
+ * Get platform-specific configuration directory
9
+ */
10
+ function getConfigDir() {
11
+ if (process.platform === 'win32') {
12
+ return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'soso');
13
+ }
14
+ return path.join(os.homedir(), '.soso');
15
+ }
16
+
17
+ /**
18
+ * Get cache directory
19
+ */
20
+ function getCacheDir() {
21
+ return path.join(getConfigDir(), 'cache');
22
+ }
23
+
24
+ /**
25
+ * Get registry file path
26
+ */
27
+ function getRegistryPath() {
28
+ return path.join(getConfigDir(), 'registry.json');
29
+ }
30
+
31
+ /**
32
+ * Get user config file path (.sosorc)
33
+ */
34
+ function getUserConfigPath() {
35
+ return path.join(os.homedir(), '.sosorc');
36
+ }
37
+
38
+ /**
39
+ * Ensure directory exists
40
+ */
41
+ function ensureDir(dirPath) {
42
+ if (!fs.existsSync(dirPath)) {
43
+ fs.mkdirSync(dirPath, { recursive: true });
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Initialize configuration directories
49
+ */
50
+ function initConfig() {
51
+ ensureDir(getConfigDir());
52
+ ensureDir(getCacheDir());
53
+
54
+ const registryPath = getRegistryPath();
55
+ if (!fs.existsSync(registryPath)) {
56
+ fs.writeFileSync(registryPath, JSON.stringify({ packages: {} }, null, 2));
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Load registry configuration
62
+ */
63
+ function loadRegistry() {
64
+ const registryPath = getRegistryPath();
65
+ if (!fs.existsSync(registryPath)) {
66
+ return { packages: {} };
67
+ }
68
+
69
+ try {
70
+ const content = fs.readFileSync(registryPath, 'utf8');
71
+ return JSON.parse(content);
72
+ } catch (error) {
73
+ throw new Error(`Failed to load registry: ${error.message}`);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Save registry configuration
79
+ */
80
+ function saveRegistry(registry) {
81
+ const registryPath = getRegistryPath();
82
+ ensureDir(path.dirname(registryPath));
83
+ fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
84
+ }
85
+
86
+ /**
87
+ * Load user configuration (.sosorc)
88
+ */
89
+ function loadUserConfig() {
90
+ const configPath = getUserConfigPath();
91
+ if (!fs.existsSync(configPath)) {
92
+ return {};
93
+ }
94
+
95
+ try {
96
+ const content = fs.readFileSync(configPath, 'utf8');
97
+ const config = {};
98
+
99
+ // Parse simple KEY=VALUE format
100
+ content.split('\n').forEach(line => {
101
+ line = line.trim();
102
+ if (line && !line.startsWith('#')) {
103
+ const [key, ...valueParts] = line.split('=');
104
+ if (key && valueParts.length > 0) {
105
+ config[key.trim()] = valueParts.join('=').trim();
106
+ }
107
+ }
108
+ });
109
+
110
+ return config;
111
+ } catch (error) {
112
+ throw new Error(`Failed to load user config: ${error.message}`);
113
+ }
114
+ }
115
+
116
+ module.exports = {
117
+ getConfigDir,
118
+ getCacheDir,
119
+ getRegistryPath,
120
+ getUserConfigPath,
121
+ ensureDir,
122
+ initConfig,
123
+ loadRegistry,
124
+ saveRegistry,
125
+ loadUserConfig
126
+ };
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { debug } = require('./utils/logger');
6
+
7
+ /**
8
+ * Lockfile manager for deterministic installs
9
+ */
10
+ class Lockfile {
11
+ constructor(projectPath) {
12
+ this.projectPath = projectPath;
13
+ this.lockfilePath = path.join(projectPath, 'soso-lock.json');
14
+ }
15
+
16
+ /**
17
+ * Check if lockfile exists
18
+ */
19
+ exists() {
20
+ return fs.existsSync(this.lockfilePath);
21
+ }
22
+
23
+ /**
24
+ * Read lockfile
25
+ */
26
+ read() {
27
+ if (!this.exists()) {
28
+ return null;
29
+ }
30
+
31
+ try {
32
+ const content = fs.readFileSync(this.lockfilePath, 'utf8');
33
+ return JSON.parse(content);
34
+ } catch (error) {
35
+ throw new Error(`Failed to read lockfile: ${error.message}`);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Write lockfile
41
+ */
42
+ write(packages) {
43
+ debug(`Writing lockfile with ${Object.keys(packages).length} packages`);
44
+
45
+ const lockfile = {
46
+ lockfileVersion: 1,
47
+ packages: {}
48
+ };
49
+
50
+ // Sort packages alphabetically for deterministic output
51
+ const sortedNames = Object.keys(packages).sort();
52
+
53
+ for (const name of sortedNames) {
54
+ const pkg = packages[name];
55
+ lockfile.packages[name] = {
56
+ version: pkg.version,
57
+ resolved: pkg.resolved,
58
+ integrity: pkg.integrity
59
+ };
60
+
61
+ // Include dependencies if present
62
+ if (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) {
63
+ lockfile.packages[name].dependencies = pkg.dependencies;
64
+ }
65
+ }
66
+
67
+ const content = JSON.stringify(lockfile, null, 2) + '\n';
68
+ fs.writeFileSync(this.lockfilePath, content);
69
+ }
70
+
71
+ /**
72
+ * Validate lockfile integrity
73
+ */
74
+ validate(lockfileData) {
75
+ if (!lockfileData) {
76
+ return false;
77
+ }
78
+
79
+ if (lockfileData.lockfileVersion !== 1) {
80
+ throw new Error('Unsupported lockfile version');
81
+ }
82
+
83
+ if (!lockfileData.packages || typeof lockfileData.packages !== 'object') {
84
+ throw new Error('Invalid lockfile format: missing packages');
85
+ }
86
+
87
+ return true;
88
+ }
89
+
90
+ /**
91
+ * Get package from lockfile
92
+ */
93
+ getPackage(lockfileData, name) {
94
+ if (!lockfileData || !lockfileData.packages) {
95
+ return null;
96
+ }
97
+ return lockfileData.packages[name] || null;
98
+ }
99
+
100
+ /**
101
+ * Check if lockfile matches current dependencies
102
+ */
103
+ matches(lockfileData, dependencies) {
104
+ if (!lockfileData || !lockfileData.packages) {
105
+ return false;
106
+ }
107
+
108
+ const lockfileNames = new Set(Object.keys(lockfileData.packages));
109
+ const currentNames = new Set(Object.keys(dependencies));
110
+
111
+ // Quick check: same number of packages
112
+ if (lockfileNames.size !== currentNames.size) {
113
+ return false;
114
+ }
115
+
116
+ // Check all dependencies are in lockfile
117
+ for (const name of currentNames) {
118
+ if (!lockfileNames.has(name)) {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ return true;
124
+ }
125
+
126
+ /**
127
+ * Delete lockfile
128
+ */
129
+ delete() {
130
+ if (this.exists()) {
131
+ fs.unlinkSync(this.lockfilePath);
132
+ }
133
+ }
134
+ }
135
+
136
+ module.exports = { Lockfile };
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ const semver = require('semver');
4
+ const { debug } = require('./utils/logger');
5
+
6
+ /**
7
+ * Resolve dependency tree to flat structure
8
+ * Implements highest-compatible-version strategy
9
+ */
10
+ class Resolver {
11
+ constructor(registry) {
12
+ this.registry = registry;
13
+ this.resolved = new Map(); // package name -> resolved version
14
+ this.pending = new Map(); // package name -> requested ranges
15
+ }
16
+
17
+ /**
18
+ * Resolve all dependencies from root package
19
+ */
20
+ async resolve(dependencies) {
21
+ if (!dependencies || Object.keys(dependencies).length === 0) {
22
+ return {};
23
+ }
24
+
25
+ // First pass: collect all version requirements
26
+ await this.collectRequirements(dependencies);
27
+
28
+ // Second pass: resolve conflicts
29
+ this.resolveConflicts();
30
+
31
+ return Object.fromEntries(this.resolved);
32
+ }
33
+
34
+ /**
35
+ * Recursively collect all dependency requirements
36
+ */
37
+ async collectRequirements(dependencies, visited = new Set()) {
38
+ for (const [name, range] of Object.entries(dependencies)) {
39
+ debug(`Collecting requirement: ${name}@${range}`);
40
+
41
+ // Track this requirement
42
+ if (!this.pending.has(name)) {
43
+ this.pending.set(name, []);
44
+ }
45
+ this.pending.get(name).push(range);
46
+
47
+ // Avoid infinite loops
48
+ const key = `${name}@${range}`;
49
+ if (visited.has(key)) {
50
+ continue;
51
+ }
52
+ visited.add(key);
53
+
54
+ // Get available versions from registry
55
+ const versions = await this.getAvailableVersions(name);
56
+ if (versions.length === 0) {
57
+ throw new Error(`Package not found in registry: ${name}`);
58
+ }
59
+
60
+ // Find highest version matching this range
61
+ const version = semver.maxSatisfying(versions, range);
62
+ if (!version) {
63
+ throw new Error(`No version of ${name} satisfies ${range}. Available: ${versions.join(', ')}`);
64
+ }
65
+
66
+ // Get package metadata to find its dependencies
67
+ const pkgInfo = await this.getPackageInfo(name, version);
68
+
69
+ // Recursively collect dependencies
70
+ if (pkgInfo.dependencies) {
71
+ await this.collectRequirements(pkgInfo.dependencies, visited);
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Resolve version conflicts
78
+ */
79
+ resolveConflicts() {
80
+ for (const [name, ranges] of this.pending.entries()) {
81
+ debug(`Resolving ${name} with ranges: ${ranges.join(', ')}`);
82
+
83
+ // Get all available versions
84
+ const packageData = this.registry.packages[name];
85
+ if (!packageData) {
86
+ throw new Error(`Package not found: ${name}`);
87
+ }
88
+
89
+ const versions = Object.keys(packageData.versions);
90
+
91
+ // Find highest version that satisfies ALL ranges
92
+ let resolvedVersion = null;
93
+
94
+ for (const version of versions.sort(semver.rcompare)) {
95
+ const satisfiesAll = ranges.every(range => semver.satisfies(version, range));
96
+ if (satisfiesAll) {
97
+ resolvedVersion = version;
98
+ break;
99
+ }
100
+ }
101
+
102
+ if (!resolvedVersion) {
103
+ throw new Error(
104
+ `Cannot resolve ${name}: conflicting version ranges ${ranges.join(', ')}. ` +
105
+ `Available versions: ${versions.join(', ')}`
106
+ );
107
+ }
108
+
109
+ this.resolved.set(name, resolvedVersion);
110
+ debug(`Resolved ${name} to ${resolvedVersion}`);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Get available versions for a package
116
+ */
117
+ async getAvailableVersions(name) {
118
+ const packageData = this.registry.packages[name];
119
+ if (!packageData || !packageData.versions) {
120
+ return [];
121
+ }
122
+
123
+ return Object.keys(packageData.versions).sort(semver.rcompare);
124
+ }
125
+
126
+ /**
127
+ * Get package info for specific version
128
+ */
129
+ async getPackageInfo(name, version) {
130
+ const packageData = this.registry.packages[name];
131
+ if (!packageData || !packageData.versions || !packageData.versions[version]) {
132
+ throw new Error(`Version ${version} not found for package ${name}`);
133
+ }
134
+
135
+ return packageData.versions[version];
136
+ }
137
+ }
138
+
139
+ module.exports = { Resolver };