Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53317afe54 | |||
| 393410a4b5 | |||
| 8507e54a56 | |||
| 0fe92d1438 | |||
| b2c926e9ae | |||
| 8add9a3dde | |||
| 79f9967db6 | |||
| 72de264b0d | |||
| 703cb25e2a | |||
| 65906f7e5f | |||
| 57e4d1c043 | |||
| f495f85bdb | |||
| d53e8fec6d | |||
| 00fef1ae06 | |||
| 4c1608cf94 | |||
| e0c4cf2983 |
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/npmextra.json"],
|
||||
"fileMatch": ["/.smartconfig.json"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
59
changelog.md
59
changelog.md
@@ -1,5 +1,64 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-24 - 3.6.0 - feat(package)
|
||||
refresh package metadata, update dependency versions, and document CLI update and install workflows
|
||||
|
||||
- rename npmextra.json to .smartconfig.json and update the published package file list
|
||||
- replace placeholder package description with CLI-specific metadata in package.json and readme
|
||||
- upgrade @git.zone/tsbuild, @git.zone/tstest, @push.rocks/smartshell, and @types/node
|
||||
- rename the bundled license file reference from LICENSE to license
|
||||
|
||||
## 2026-02-09 - 3.5.0 - feat(install)
|
||||
add interactive install command and module to detect and install missing @git.zone packages
|
||||
|
||||
- Add ts/mod_install/index.ts: implements interactive/non-interactive flow to detect package managers, collect installed @git.zone packages, prompt user (via smartinteract) and install selected packages via PackageManagerUtil.
|
||||
- Add ts/mod_install/mod.plugins.ts: re-export smartinteract and smartshell for the installer.
|
||||
- Update ts/tools.cli.ts: register new 'install' command and add help text for install flags.
|
||||
- Update ts/mod_update/index.ts: export GITZONE_PACKAGES and print a summary of managed packages that are not installed with latest versions and a suggestion to run 'gtools install'.
|
||||
|
||||
## 2026-02-09 - 3.4.1 - fix(tools)
|
||||
no changes to commit
|
||||
|
||||
- No files changed in this diff
|
||||
- Current package version is 3.4.0 — no version bump required
|
||||
|
||||
## 2026-02-09 - 3.4.0 - feat(mod_update)
|
||||
add @git.zone/tsrust to supported modules list
|
||||
|
||||
- ts/mod_update/index.ts: added '@git.zone/tsrust' to the modules array
|
||||
|
||||
## 2026-02-06 - 3.3.0 - feat(mod_update)
|
||||
add self-update flow, package name parser, dynamic CLI version, and tests
|
||||
|
||||
- Add a self-update check in mod_update to detect and optionally update @git.zone/tools (prompts the user or uses --yes).
|
||||
- Introduce PackageManagerUtil.parseYarnPackageName to correctly parse scoped and unscoped yarn package strings and use it when collecting installed packages.
|
||||
- Add comprehensive unit tests for PackageManagerUtil.isNewerVersion and parseYarnPackageName.
|
||||
- Use commitinfo.version for CLI reported version instead of a hardcoded value.
|
||||
- Remove automatic invocation of runCli() from ts/index.ts to avoid immediate execution on import.
|
||||
|
||||
## 2026-02-05 - 3.2.0 - feat(update)
|
||||
enhance package manager detection, version reporting, and add verbose option
|
||||
|
||||
- Add IPackageManagerInfo interface and detectPackageManager() to robustly detect npm/yarn/pnpm via 'which' and '--version' fallbacks
|
||||
- Make isAvailable() delegate to detectPackageManager() and return structured detection info
|
||||
- Add getPackageManagerVersion() to obtain current and latest versions (parses local --version and queries npm registry)
|
||||
- Update run() to support a verbose flag, show a package-manager status table, and collect detectedPMs with version/update status
|
||||
- Update CLI help and command handling to accept --verbose/-v and pass it through to mod_update.run()
|
||||
|
||||
## 2026-02-03 - 3.1.3 - fix(mod_update)
|
||||
try private registry (verdaccio.lossless.digital) first when fetching package versions; fall back to public npm; handle unknown latest versions gracefully in output
|
||||
|
||||
- getLatestVersion now attempts a direct API request to https://verdaccio.lossless.digital/<encoded-package> and parses dist-tags.latest
|
||||
- Falls back to npm view when the private registry request fails
|
||||
- Scoped package names are URL-encoded (replaces '/' with '%2f') before querying the private registry
|
||||
- Packages with no resolvable latest version are included with latestVersion set to 'unknown' and displayed as '? Version unknown'
|
||||
- needsUpdate is set to false when latest version is unknown
|
||||
|
||||
## 2026-02-03 - 3.1.2 - fix(scripts)
|
||||
make test script output verbose by using --verbose instead of --web
|
||||
|
||||
- package.json: change npm "test" script from "(tstest test/ --web)" to "(tstest test/ --verbose)" to enable verbose test output
|
||||
|
||||
## 2026-02-03 - 3.1.1 - fix(tools)
|
||||
no changes detected
|
||||
|
||||
|
||||
16
package.json
16
package.json
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@git.zone/tools",
|
||||
"version": "3.1.1",
|
||||
"version": "3.6.0",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
"description": "A CLI tool placeholder for development utilities.",
|
||||
"description": "A CLI utility for managing and updating @git.zone development tools across package managers.",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --web)",
|
||||
"test": "(tstest test/ --verbose)",
|
||||
"build": "(tsbuild --web)"
|
||||
},
|
||||
"bin": {
|
||||
@@ -30,15 +30,15 @@
|
||||
},
|
||||
"homepage": "https://github.com/GitZoneTools/npmg#readme",
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^4.1.2",
|
||||
"@git.zone/tstest": "^3.1.8"
|
||||
"@git.zone/tsbuild": "^4.3.0",
|
||||
"@git.zone/tstest": "^3.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@push.rocks/smartcli": "^4.0.20",
|
||||
"@push.rocks/smartinteract": "^2.0.16",
|
||||
"@push.rocks/smartshell": "^3.3.0",
|
||||
"@types/node": "^22.0.0"
|
||||
"@push.rocks/smartshell": "^3.3.8",
|
||||
"@types/node": "^25.5.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -48,7 +48,7 @@
|
||||
"dist_ts/**/*",
|
||||
"dist_ts_web/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
".smartconfig.json",
|
||||
"readme.md"
|
||||
],
|
||||
"browserslist": [
|
||||
|
||||
2400
pnpm-lock.yaml
generated
2400
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
125
readme.md
125
readme.md
@@ -1,6 +1,6 @@
|
||||
# @git.zone/tools 🛠️
|
||||
|
||||
A CLI tool placeholder for development utilities.
|
||||
A powerful CLI utility for managing your `@git.zone` development toolchain. Scan, update, and install all official `@git.zone` packages across multiple package managers — from a single command.
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
@@ -8,21 +8,138 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
|
||||
## Installation 📦
|
||||
|
||||
Install globally with your preferred package manager:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install -g @git.zone/tools
|
||||
|
||||
# pnpm (recommended)
|
||||
pnpm add -g @git.zone/tools
|
||||
|
||||
# yarn
|
||||
yarn global add @git.zone/tools
|
||||
```
|
||||
|
||||
This gives you the `gtools` binary.
|
||||
|
||||
## Usage 🚀
|
||||
|
||||
### Overview
|
||||
|
||||
```bash
|
||||
gtools
|
||||
gtools # Show available commands
|
||||
gtools update # Check & update installed @git.zone packages
|
||||
gtools install # Interactively install missing @git.zone packages
|
||||
```
|
||||
|
||||
Currently a placeholder CLI with no commands implemented yet.
|
||||
### `gtools update` — Keep Everything Fresh ♻️
|
||||
|
||||
Scans all available package managers (npm, pnpm, yarn) on your system, detects globally installed `@git.zone` packages, and checks them against the latest published versions.
|
||||
|
||||
```bash
|
||||
gtools update # Interactive — prompts before updating
|
||||
gtools update -y # Auto-approve all updates (great for CI)
|
||||
gtools update --verbose # Show package manager detection diagnostics
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
|
||||
1. 🔍 Detects which package managers are available on your system
|
||||
2. 📊 Shows a version table for each detected package manager
|
||||
3. 🔄 Checks for a **self-update** of `gtools` itself first
|
||||
4. 📋 Lists all globally installed `@git.zone` packages with current vs. latest versions
|
||||
5. 🚀 Prompts you to update any outdated packages (or auto-updates with `-y`)
|
||||
|
||||
**Example output:**
|
||||
|
||||
```
|
||||
Package managers:
|
||||
|
||||
Name Current Latest Status
|
||||
──────────────────────────────────────────────
|
||||
pnpm 10.28.2 10.28.2 ✓ Up to date
|
||||
|
||||
Installed @git.zone packages:
|
||||
|
||||
Package Current Latest PM Status
|
||||
─────────────────────────────────────────────────────────────────────
|
||||
@git.zone/cli 1.10.0 1.10.0 pnpm ✓ Up to date
|
||||
@git.zone/tsbuild 4.3.0 4.3.0 pnpm ✓ Up to date
|
||||
@git.zone/tstest 3.5.1 3.5.1 pnpm ✓ Up to date
|
||||
|
||||
All packages are up to date!
|
||||
```
|
||||
|
||||
### `gtools install` — Bootstrap Your Toolchain 🧰
|
||||
|
||||
Detects which official `@git.zone` packages are **not** installed and lets you pick which ones to add.
|
||||
|
||||
```bash
|
||||
gtools install # Interactive selection
|
||||
gtools install -y # Install all missing packages automatically
|
||||
gtools install --verbose # Show detection diagnostics
|
||||
```
|
||||
|
||||
**Managed packages:**
|
||||
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@git.zone/cli` | The main gitzone CLI (`gitzone` command) |
|
||||
| `@git.zone/tsbuild` | TypeScript compiler wrapper |
|
||||
| `@git.zone/tstest` | Test runner for TypeScript projects |
|
||||
| `@git.zone/tsbundle` | Bundle TypeScript for browsers |
|
||||
| `@git.zone/tswatch` | File watcher with live reload |
|
||||
| `@git.zone/tspublish` | Publish packages to npm registries |
|
||||
| `@git.zone/tsdoc` | Documentation generator |
|
||||
| `@git.zone/tsdocker` | Docker integration for TS projects |
|
||||
| `@git.zone/tsview` | Component viewer |
|
||||
| `@git.zone/tsrust` | Rust cross-compilation support |
|
||||
|
||||
### Smart Package Manager Detection 🔎
|
||||
|
||||
`gtools` doesn't assume which package manager you use. It probes for `npm`, `pnpm`, and `yarn` using multiple detection strategies:
|
||||
|
||||
1. **`which` command** — checks the system PATH
|
||||
2. **`--version` fallback** — directly invokes the PM if `which` fails
|
||||
|
||||
When installing, it recommends the package manager that already has the most `@git.zone` packages, so your toolchain stays consistent.
|
||||
|
||||
## Programmatic API 🔧
|
||||
|
||||
You can also use `@git.zone/tools` as a library:
|
||||
|
||||
```typescript
|
||||
import { runCli } from '@git.zone/tools';
|
||||
|
||||
// Launch the CLI programmatically
|
||||
await runCli();
|
||||
```
|
||||
|
||||
Or use the `PackageManagerUtil` class directly for custom tooling:
|
||||
|
||||
```typescript
|
||||
import { PackageManagerUtil } from '@git.zone/tools/dist_ts/mod_update/classes.packagemanager.js';
|
||||
|
||||
const pmUtil = new PackageManagerUtil();
|
||||
|
||||
// Check if pnpm is available
|
||||
const pnpmInfo = await pmUtil.detectPackageManager('pnpm');
|
||||
console.log(pnpmInfo.available); // true/false
|
||||
|
||||
// Get globally installed @git.zone packages
|
||||
const packages = await pmUtil.getInstalledPackages('pnpm');
|
||||
|
||||
// Get latest version from registry (checks private registry first, then npm)
|
||||
const latest = await pmUtil.getLatestVersion('@git.zone/tsbuild');
|
||||
|
||||
// Compare versions
|
||||
pmUtil.isNewerVersion('1.0.0', '2.0.0'); // true
|
||||
```
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
|
||||
@@ -1,9 +1,93 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
import * as tools from '../ts/index.js';
|
||||
import { PackageManagerUtil } from '../ts/mod_update/classes.packagemanager.js';
|
||||
|
||||
tap.test('should export runCli function', async () => {
|
||||
expect(typeof tools.runCli).toEqual('function');
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// isNewerVersion tests
|
||||
// ============================================
|
||||
|
||||
tap.test('isNewerVersion: should detect newer major version', async () => {
|
||||
const pmUtil = new PackageManagerUtil();
|
||||
expect(pmUtil.isNewerVersion('1.0.0', '2.0.0')).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('isNewerVersion: should detect newer minor version', async () => {
|
||||
const pmUtil = new PackageManagerUtil();
|
||||
expect(pmUtil.isNewerVersion('1.0.0', '1.1.0')).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('isNewerVersion: should detect newer patch version', async () => {
|
||||
const pmUtil = new PackageManagerUtil();
|
||||
expect(pmUtil.isNewerVersion('1.0.0', '1.0.1')).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('isNewerVersion: should return false for equal versions', async () => {
|
||||
const pmUtil = new PackageManagerUtil();
|
||||
expect(pmUtil.isNewerVersion('1.0.0', '1.0.0')).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('isNewerVersion: should return false when current is newer', async () => {
|
||||
const pmUtil = new PackageManagerUtil();
|
||||
expect(pmUtil.isNewerVersion('2.0.0', '1.0.0')).toBeFalse();
|
||||
expect(pmUtil.isNewerVersion('1.1.0', '1.0.0')).toBeFalse();
|
||||
expect(pmUtil.isNewerVersion('1.0.1', '1.0.0')).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('isNewerVersion: should handle v-prefixed versions', async () => {
|
||||
const pmUtil = new PackageManagerUtil();
|
||||
expect(pmUtil.isNewerVersion('v1.0.0', 'v2.0.0')).toBeTrue();
|
||||
expect(pmUtil.isNewerVersion('v2.0.0', 'v1.0.0')).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('isNewerVersion: should handle mixed prefixed and non-prefixed', async () => {
|
||||
const pmUtil = new PackageManagerUtil();
|
||||
expect(pmUtil.isNewerVersion('v1.0.0', '2.0.0')).toBeTrue();
|
||||
expect(pmUtil.isNewerVersion('1.0.0', 'v2.0.0')).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('isNewerVersion: should handle versions with different segment counts', async () => {
|
||||
const pmUtil = new PackageManagerUtil();
|
||||
expect(pmUtil.isNewerVersion('1.0', '1.0.1')).toBeTrue();
|
||||
expect(pmUtil.isNewerVersion('1.0.1', '1.0')).toBeFalse();
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// parseYarnPackageName tests
|
||||
// ============================================
|
||||
|
||||
tap.test('parseYarnPackageName: should parse scoped package correctly', async () => {
|
||||
const result = PackageManagerUtil.parseYarnPackageName('@git.zone/cli@1.2.3');
|
||||
expect(result.name).toEqual('@git.zone/cli');
|
||||
expect(result.version).toEqual('1.2.3');
|
||||
});
|
||||
|
||||
tap.test('parseYarnPackageName: should parse unscoped package correctly', async () => {
|
||||
const result = PackageManagerUtil.parseYarnPackageName('typescript@5.3.2');
|
||||
expect(result.name).toEqual('typescript');
|
||||
expect(result.version).toEqual('5.3.2');
|
||||
});
|
||||
|
||||
tap.test('parseYarnPackageName: should handle empty string', async () => {
|
||||
const result = PackageManagerUtil.parseYarnPackageName('');
|
||||
expect(result.name).toEqual('');
|
||||
expect(result.version).toEqual('unknown');
|
||||
});
|
||||
|
||||
tap.test('parseYarnPackageName: should handle name with no version', async () => {
|
||||
const result = PackageManagerUtil.parseYarnPackageName('@git.zone/cli');
|
||||
expect(result.name).toEqual('@git.zone/cli');
|
||||
expect(result.version).toEqual('unknown');
|
||||
});
|
||||
|
||||
tap.test('parseYarnPackageName: should handle unscoped name with no version', async () => {
|
||||
const result = PackageManagerUtil.parseYarnPackageName('typescript');
|
||||
expect(result.name).toEqual('typescript');
|
||||
expect(result.version).toEqual('unknown');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tools',
|
||||
version: '3.1.1',
|
||||
description: 'A CLI tool placeholder for development utilities.'
|
||||
version: '3.6.0',
|
||||
description: 'A CLI utility for managing and updating @git.zone development tools across package managers.'
|
||||
}
|
||||
|
||||
@@ -3,5 +3,3 @@ import * as cli from './tools.cli.js';
|
||||
export const runCli = async () => {
|
||||
await cli.run();
|
||||
};
|
||||
|
||||
runCli();
|
||||
|
||||
151
ts/mod_install/index.ts
Normal file
151
ts/mod_install/index.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import * as plugins from './mod.plugins.js';
|
||||
import { PackageManagerUtil, type TPackageManager, type IPackageManagerInfo, type IInstalledPackage } from '../mod_update/classes.packagemanager.js';
|
||||
import { GITZONE_PACKAGES } from '../mod_update/index.js';
|
||||
|
||||
export interface IInstallOptions {
|
||||
yes?: boolean;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export const run = async (options: IInstallOptions = {}): Promise<void> => {
|
||||
const pmUtil = new PackageManagerUtil();
|
||||
const verbose = options.verbose === true;
|
||||
|
||||
console.log('Scanning for missing @git.zone packages...\n');
|
||||
|
||||
// 1. Detect available package managers
|
||||
const detectedPMs: IPackageManagerInfo[] = [];
|
||||
for (const pm of ['npm', 'yarn', 'pnpm'] as TPackageManager[]) {
|
||||
const info = await pmUtil.detectPackageManager(pm, verbose);
|
||||
if (info.available) {
|
||||
detectedPMs.push(info);
|
||||
}
|
||||
}
|
||||
|
||||
if (detectedPMs.length === 0) {
|
||||
console.log('No package managers found (npm, yarn, pnpm).');
|
||||
return;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
console.log(`Detected package managers: ${detectedPMs.map(p => p.name).join(', ')}\n`);
|
||||
}
|
||||
|
||||
// 2. Collect all globally installed @git.zone packages across all PMs
|
||||
const installedByPm = new Map<TPackageManager, IInstalledPackage[]>();
|
||||
const allInstalledNames = new Set<string>();
|
||||
|
||||
for (const pmInfo of detectedPMs) {
|
||||
const installed = await pmUtil.getInstalledPackages(pmInfo.name);
|
||||
installedByPm.set(pmInfo.name, installed);
|
||||
for (const pkg of installed) {
|
||||
if (GITZONE_PACKAGES.includes(pkg.name)) {
|
||||
allInstalledNames.add(pkg.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Determine which managed packages are not installed
|
||||
const notInstalled = GITZONE_PACKAGES.filter(name => !allInstalledNames.has(name));
|
||||
|
||||
if (notInstalled.length === 0) {
|
||||
console.log('All managed @git.zone packages are already installed.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${notInstalled.length} package(s) not installed.\n`);
|
||||
|
||||
// 4. Determine the best default PM (the one with most @git.zone packages)
|
||||
let bestPm = detectedPMs[0].name;
|
||||
let bestCount = 0;
|
||||
for (const pmInfo of detectedPMs) {
|
||||
const pkgs = installedByPm.get(pmInfo.name) || [];
|
||||
const gitzoneCount = pkgs.filter(p => GITZONE_PACKAGES.includes(p.name)).length;
|
||||
if (gitzoneCount > bestCount) {
|
||||
bestCount = gitzoneCount;
|
||||
bestPm = pmInfo.name;
|
||||
}
|
||||
}
|
||||
|
||||
let selectedPm: TPackageManager;
|
||||
let selectedPackages: string[];
|
||||
|
||||
if (options.yes === true) {
|
||||
// Non-interactive: use best PM, install all missing
|
||||
selectedPm = bestPm;
|
||||
selectedPackages = notInstalled;
|
||||
console.log(`Using ${selectedPm} (auto-detected).\n`);
|
||||
} else {
|
||||
// 5. Ask which PM to use
|
||||
const smartinteractInstance = new plugins.smartinteract.SmartInteract();
|
||||
|
||||
if (detectedPMs.length === 1) {
|
||||
selectedPm = detectedPMs[0].name;
|
||||
console.log(`Using ${selectedPm} (only available PM).\n`);
|
||||
} else {
|
||||
const pmAnswer = await smartinteractInstance.askQuestion({
|
||||
name: 'packageManager',
|
||||
type: 'list',
|
||||
message: 'Which package manager should be used for installation?',
|
||||
default: bestPm,
|
||||
choices: detectedPMs.map(pm => ({
|
||||
name: `${pm.name}${pm.name === bestPm ? ' (recommended — most @git.zone packages)' : ''}`,
|
||||
value: pm.name,
|
||||
})),
|
||||
});
|
||||
selectedPm = pmAnswer.value as TPackageManager;
|
||||
}
|
||||
|
||||
// 6. Ask which packages to install
|
||||
// Fetch latest versions for display
|
||||
const choicesWithVersions: Array<{ name: string; value: string }> = [];
|
||||
for (const pkgName of notInstalled) {
|
||||
const latest = await pmUtil.getLatestVersion(pkgName);
|
||||
const versionLabel = latest ? `@${latest}` : '';
|
||||
choicesWithVersions.push({
|
||||
name: `${pkgName}${versionLabel}`,
|
||||
value: pkgName,
|
||||
});
|
||||
}
|
||||
|
||||
const pkgAnswer = await smartinteractInstance.askQuestion({
|
||||
name: 'packages',
|
||||
type: 'checkbox',
|
||||
message: 'Select packages to install:',
|
||||
default: notInstalled, // all pre-checked
|
||||
choices: choicesWithVersions,
|
||||
});
|
||||
|
||||
selectedPackages = pkgAnswer.value as string[];
|
||||
|
||||
if (selectedPackages.length === 0) {
|
||||
console.log('No packages selected. Nothing to install.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Install selected packages
|
||||
console.log(`Installing ${selectedPackages.length} package(s) via ${selectedPm}...\n`);
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const pkgName of selectedPackages) {
|
||||
const success = await pmUtil.executeUpdate(selectedPm, pkgName);
|
||||
if (success) {
|
||||
console.log(` ✓ ${pkgName} installed successfully`);
|
||||
successCount++;
|
||||
} else {
|
||||
console.log(` ✗ ${pkgName} installation failed`);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Summary
|
||||
console.log('');
|
||||
if (failCount === 0) {
|
||||
console.log(`All ${successCount} package(s) installed successfully!`);
|
||||
} else {
|
||||
console.log(`Installed ${successCount} package(s), ${failCount} failed.`);
|
||||
}
|
||||
};
|
||||
4
ts/mod_install/mod.plugins.ts
Normal file
4
ts/mod_install/mod.plugins.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import * as smartinteract from '@push.rocks/smartinteract';
|
||||
import * as smartshell from '@push.rocks/smartshell';
|
||||
|
||||
export { smartinteract, smartshell };
|
||||
@@ -16,6 +16,16 @@ export interface IPackageUpdateInfo {
|
||||
needsUpdate: boolean;
|
||||
}
|
||||
|
||||
export interface IPackageManagerInfo {
|
||||
name: TPackageManager;
|
||||
available: boolean;
|
||||
detectionMethod?: 'which' | 'version-command';
|
||||
path?: string;
|
||||
currentVersion?: string;
|
||||
latestVersion?: string | null;
|
||||
needsUpdate?: boolean;
|
||||
}
|
||||
|
||||
export class PackageManagerUtil {
|
||||
private shell = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
@@ -23,14 +33,92 @@ export class PackageManagerUtil {
|
||||
|
||||
/**
|
||||
* Check if a package manager is available on the system
|
||||
* Uses multiple detection methods for robustness across different shell contexts
|
||||
*/
|
||||
public async isAvailable(pm: TPackageManager): Promise<boolean> {
|
||||
public async isAvailable(pm: TPackageManager, verbose = false): Promise<boolean> {
|
||||
const info = await this.detectPackageManager(pm, verbose);
|
||||
return info.available;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect a package manager and return detailed info
|
||||
*/
|
||||
public async detectPackageManager(pm: TPackageManager, verbose = false): Promise<IPackageManagerInfo> {
|
||||
const info: IPackageManagerInfo = { name: pm, available: false };
|
||||
|
||||
// Primary method: try 'which' command
|
||||
try {
|
||||
const result = await this.shell.execSilent(`which ${pm} >/dev/null 2>&1 && echo "found"`);
|
||||
return result.exitCode === 0 && result.stdout.includes('found');
|
||||
const whichResult = await this.shell.execSilent(`which ${pm} 2>/dev/null`);
|
||||
if (whichResult.exitCode === 0 && whichResult.stdout.trim()) {
|
||||
info.available = true;
|
||||
info.detectionMethod = 'which';
|
||||
info.path = whichResult.stdout.trim();
|
||||
if (verbose) {
|
||||
console.log(` Checking ${pm}... found via 'which' at ${info.path}`);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
// Continue to fallback
|
||||
}
|
||||
|
||||
// Fallback method: try running pm --version directly
|
||||
// This can find PMs that are available but not in PATH for 'which'
|
||||
try {
|
||||
const versionResult = await this.shell.execSilent(`${pm} --version 2>/dev/null`);
|
||||
if (versionResult.exitCode === 0 && versionResult.stdout.trim()) {
|
||||
info.available = true;
|
||||
info.detectionMethod = 'version-command';
|
||||
if (verbose) {
|
||||
console.log(` Checking ${pm}... found via '--version' (which failed)`);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
} catch {
|
||||
// Not available
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
console.log(` Checking ${pm}... not found`);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current and latest version of a package manager
|
||||
*/
|
||||
public async getPackageManagerVersion(pm: TPackageManager): Promise<{ current: string; latest: string | null }> {
|
||||
let current = 'unknown';
|
||||
let latest: string | null = null;
|
||||
|
||||
// Get current version
|
||||
try {
|
||||
const result = await this.shell.execSilent(`${pm} --version 2>/dev/null`);
|
||||
if (result.exitCode === 0 && result.stdout.trim()) {
|
||||
// Parse version from output - handle different formats
|
||||
const output = result.stdout.trim();
|
||||
// npm: "10.2.0", pnpm: "8.15.0", yarn: "1.22.19"
|
||||
// Some may include prefix like "v1.22.19"
|
||||
const versionMatch = output.match(/(\d+\.\d+\.\d+)/);
|
||||
if (versionMatch) {
|
||||
current = versionMatch[1];
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Keep as unknown
|
||||
}
|
||||
|
||||
// Get latest version from npm registry
|
||||
try {
|
||||
const result = await this.shell.execSilent(`npm view ${pm} version 2>/dev/null`);
|
||||
if (result.exitCode === 0 && result.stdout.trim()) {
|
||||
latest = result.stdout.trim();
|
||||
}
|
||||
} catch {
|
||||
// Keep as null
|
||||
}
|
||||
|
||||
return { current, latest };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,12 +191,11 @@ export class PackageManagerUtil {
|
||||
const data = JSON.parse(line);
|
||||
if (data.type === 'tree' && data.data && data.data.trees) {
|
||||
for (const tree of data.data.trees) {
|
||||
const name = tree.name?.split('@')[0] || '';
|
||||
if (name.startsWith('@git.zone/')) {
|
||||
const version = tree.name?.split('@').pop() || 'unknown';
|
||||
const parsed = PackageManagerUtil.parseYarnPackageName(tree.name || '');
|
||||
if (parsed.name.startsWith('@git.zone/')) {
|
||||
packages.push({
|
||||
name,
|
||||
version,
|
||||
name: parsed.name,
|
||||
version: parsed.version,
|
||||
packageManager: pm,
|
||||
});
|
||||
}
|
||||
@@ -128,8 +215,28 @@ export class PackageManagerUtil {
|
||||
|
||||
/**
|
||||
* Get the latest version of a package from npm registry
|
||||
* Tries private registry (verdaccio.lossless.digital) first via API, then falls back to public npm
|
||||
*/
|
||||
public async getLatestVersion(packageName: string): Promise<string | null> {
|
||||
// URL-encode the package name for scoped packages (@scope/name -> @scope%2fname)
|
||||
const encodedName = packageName.replace('/', '%2f');
|
||||
|
||||
// Try private registry first via direct API call (npm view doesn't work reliably)
|
||||
try {
|
||||
const result = await this.shell.execSilent(
|
||||
`curl -sf "https://verdaccio.lossless.digital/${encodedName}" 2>/dev/null`
|
||||
);
|
||||
if (result.exitCode === 0 && result.stdout.trim()) {
|
||||
const data = JSON.parse(result.stdout.trim());
|
||||
if (data['dist-tags']?.latest) {
|
||||
return data['dist-tags'].latest;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Continue to public registry
|
||||
}
|
||||
|
||||
// Fall back to public npm
|
||||
try {
|
||||
const result = await this.shell.execSilent(`npm view ${packageName} version 2>/dev/null`);
|
||||
if (result.exitCode === 0 && result.stdout.trim()) {
|
||||
@@ -169,6 +276,26 @@ export class PackageManagerUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a yarn package name string like "@git.zone/cli@1.0.0" into name and version.
|
||||
* Handles scoped packages correctly by splitting on the last '@' (version separator).
|
||||
*/
|
||||
public static parseYarnPackageName(fullName: string): { name: string; version: string } {
|
||||
if (!fullName) {
|
||||
return { name: '', version: 'unknown' };
|
||||
}
|
||||
const lastAtIndex = fullName.lastIndexOf('@');
|
||||
// If lastAtIndex is 0, the string is just "@something" with no version
|
||||
// If lastAtIndex is -1, there's no '@' at all
|
||||
if (lastAtIndex <= 0) {
|
||||
return { name: fullName, version: 'unknown' };
|
||||
}
|
||||
return {
|
||||
name: fullName.substring(0, lastAtIndex),
|
||||
version: fullName.substring(lastAtIndex + 1) || 'unknown',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two semver versions
|
||||
* Returns true if latest > current
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import * as plugins from './mod.plugins.js';
|
||||
import { PackageManagerUtil, type TPackageManager, type IPackageUpdateInfo } from './classes.packagemanager.js';
|
||||
import { PackageManagerUtil, type TPackageManager, type IPackageUpdateInfo, type IPackageManagerInfo } from './classes.packagemanager.js';
|
||||
import { commitinfo } from '../00_commitinfo_data.js';
|
||||
|
||||
const GITZONE_PACKAGES = [
|
||||
// Curated list of known @git.zone CLI tools to track for updates.
|
||||
// This list is intentionally hardcoded to only track official tools.
|
||||
// Add new entries here when new @git.zone packages are published.
|
||||
export const GITZONE_PACKAGES = [
|
||||
'@git.zone/cli',
|
||||
'@git.zone/tsdoc',
|
||||
'@git.zone/tsbuild',
|
||||
@@ -11,50 +15,133 @@ const GITZONE_PACKAGES = [
|
||||
'@git.zone/tsdocker',
|
||||
'@git.zone/tsview',
|
||||
'@git.zone/tswatch',
|
||||
'@git.zone/tsrust',
|
||||
];
|
||||
|
||||
export interface IUpdateOptions {
|
||||
yes?: boolean;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export const run = async (options: IUpdateOptions = {}): Promise<void> => {
|
||||
const pmUtil = new PackageManagerUtil();
|
||||
const verbose = options.verbose === true;
|
||||
|
||||
console.log('Scanning for installed @git.zone packages...\n');
|
||||
|
||||
// Check which package managers are available
|
||||
const availablePMs: TPackageManager[] = [];
|
||||
if (verbose) {
|
||||
console.log('Detecting package managers:');
|
||||
}
|
||||
|
||||
const detectedPMs: IPackageManagerInfo[] = [];
|
||||
for (const pm of ['npm', 'yarn', 'pnpm'] as TPackageManager[]) {
|
||||
if (await pmUtil.isAvailable(pm)) {
|
||||
availablePMs.push(pm);
|
||||
const info = await pmUtil.detectPackageManager(pm, verbose);
|
||||
if (info.available) {
|
||||
detectedPMs.push(info);
|
||||
}
|
||||
}
|
||||
|
||||
if (availablePMs.length === 0) {
|
||||
if (verbose) {
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (detectedPMs.length === 0) {
|
||||
console.log('No package managers found (npm, yarn, pnpm).');
|
||||
console.log('Tried detection via \'which\' command and direct version check.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found package managers: ${availablePMs.join(', ')}\n`);
|
||||
// Get version info for each PM and display status table
|
||||
console.log('Package managers:\n');
|
||||
console.log(' Name Current Latest Status');
|
||||
console.log(' ──────────────────────────────────────────────');
|
||||
|
||||
for (const pmInfo of detectedPMs) {
|
||||
const versionInfo = await pmUtil.getPackageManagerVersion(pmInfo.name);
|
||||
pmInfo.currentVersion = versionInfo.current;
|
||||
pmInfo.latestVersion = versionInfo.latest;
|
||||
pmInfo.needsUpdate = versionInfo.latest
|
||||
? pmUtil.isNewerVersion(versionInfo.current, versionInfo.latest)
|
||||
: false;
|
||||
|
||||
const name = pmInfo.name.padEnd(9);
|
||||
const current = versionInfo.current.padEnd(12);
|
||||
const latest = (versionInfo.latest || 'unknown').padEnd(12);
|
||||
const status = versionInfo.latest === null
|
||||
? '? Version unknown'
|
||||
: pmInfo.needsUpdate
|
||||
? '⬆️ Update available'
|
||||
: '✓ Up to date';
|
||||
console.log(` ${name}${current}${latest}${status}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// === Self-update check ===
|
||||
console.log('Checking for gtools self-update...\n');
|
||||
const selfVersion = commitinfo.version;
|
||||
const selfLatest = await pmUtil.getLatestVersion('@git.zone/tools');
|
||||
|
||||
if (selfLatest && pmUtil.isNewerVersion(selfVersion, selfLatest)) {
|
||||
console.log(` @git.zone/tools ${selfVersion} → ${selfLatest} ⬆️ Update available\n`);
|
||||
|
||||
// Find which PM has it installed globally
|
||||
let selfPm: TPackageManager | null = null;
|
||||
for (const pmInfo of detectedPMs) {
|
||||
const installed = await pmUtil.getInstalledPackages(pmInfo.name);
|
||||
if (installed.some(p => p.name === '@git.zone/tools')) {
|
||||
selfPm = pmInfo.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!selfPm) {
|
||||
// Fallback: use first available PM
|
||||
selfPm = detectedPMs[0].name;
|
||||
}
|
||||
|
||||
let shouldSelfUpdate = options.yes === true;
|
||||
if (!shouldSelfUpdate) {
|
||||
const smartinteractInstance = new plugins.smartinteract.SmartInteract();
|
||||
const answer = await smartinteractInstance.askQuestion({
|
||||
type: 'confirm',
|
||||
name: 'confirmSelfUpdate',
|
||||
message: 'Do you want to update gtools itself first?',
|
||||
default: true,
|
||||
});
|
||||
shouldSelfUpdate = answer.value === true;
|
||||
}
|
||||
|
||||
if (shouldSelfUpdate) {
|
||||
const success = await pmUtil.executeUpdate(selfPm, '@git.zone/tools');
|
||||
if (success) {
|
||||
console.log('\ngtools has been updated. Please re-run "gtools update" to check remaining packages.');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('\ngtools self-update failed. Continuing with current version...\n');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(` @git.zone/tools ${selfVersion} ✓ Up to date\n`);
|
||||
}
|
||||
|
||||
// Collect all installed @git.zone packages from all package managers
|
||||
const allPackages: IPackageUpdateInfo[] = [];
|
||||
|
||||
for (const pm of availablePMs) {
|
||||
const installed = await pmUtil.getInstalledPackages(pm);
|
||||
for (const pmInfo of detectedPMs) {
|
||||
const installed = await pmUtil.getInstalledPackages(pmInfo.name);
|
||||
for (const pkg of installed) {
|
||||
// Only include packages from our predefined list
|
||||
if (GITZONE_PACKAGES.includes(pkg.name)) {
|
||||
const latestVersion = await pmUtil.getLatestVersion(pkg.name);
|
||||
if (latestVersion) {
|
||||
allPackages.push({
|
||||
name: pkg.name,
|
||||
currentVersion: pkg.version,
|
||||
latestVersion,
|
||||
packageManager: pm,
|
||||
needsUpdate: pmUtil.isNewerVersion(pkg.version, latestVersion),
|
||||
});
|
||||
}
|
||||
allPackages.push({
|
||||
name: pkg.name,
|
||||
currentVersion: pkg.version,
|
||||
latestVersion: latestVersion || 'unknown',
|
||||
packageManager: pmInfo.name,
|
||||
needsUpdate: latestVersion ? pmUtil.isNewerVersion(pkg.version, latestVersion) : false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,12 +161,34 @@ export const run = async (options: IUpdateOptions = {}): Promise<void> => {
|
||||
const current = pkg.currentVersion.padEnd(12);
|
||||
const latest = pkg.latestVersion.padEnd(12);
|
||||
const pm = pkg.packageManager.padEnd(8);
|
||||
const status = pkg.needsUpdate ? '⬆️ Update available' : '✓ Up to date';
|
||||
const status = pkg.latestVersion === 'unknown'
|
||||
? '? Version unknown'
|
||||
: pkg.needsUpdate
|
||||
? '⬆️ Update available'
|
||||
: '✓ Up to date';
|
||||
console.log(` ${name}${current}${latest}${pm}${status}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// Show managed packages that are not installed anywhere
|
||||
const installedNames = new Set(allPackages.map(p => p.name));
|
||||
const notInstalled = GITZONE_PACKAGES.filter(name => !installedNames.has(name));
|
||||
|
||||
if (notInstalled.length > 0) {
|
||||
console.log('Not installed (managed @git.zone packages):\n');
|
||||
console.log(' Package Latest');
|
||||
console.log(' ─────────────────────────────────────────');
|
||||
for (const pkgName of notInstalled) {
|
||||
const latest = await pmUtil.getLatestVersion(pkgName);
|
||||
const name = pkgName.padEnd(28);
|
||||
const version = latest || 'unknown';
|
||||
console.log(` ${name} ${version}`);
|
||||
}
|
||||
console.log('');
|
||||
console.log(' Run "gtools install" to install missing packages.\n');
|
||||
}
|
||||
|
||||
// Filter packages that need updates
|
||||
const packagesToUpdate = allPackages.filter(p => p.needsUpdate);
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as plugins from './tools.plugins.js';
|
||||
import * as modUpdate from './mod_update/index.js';
|
||||
import * as modInstall from './mod_install/index.js';
|
||||
import { commitinfo } from './00_commitinfo_data.js';
|
||||
|
||||
export const run = async () => {
|
||||
const toolsCli = new plugins.smartcli.Smartcli();
|
||||
@@ -7,17 +9,28 @@ export const run = async () => {
|
||||
toolsCli.standardCommand().subscribe(async (argvArg) => {
|
||||
console.log('@git.zone/tools - CLI utility for managing @git.zone packages\n');
|
||||
console.log('Commands:');
|
||||
console.log(' update Check and update globally installed @git.zone packages');
|
||||
console.log(' update -y Update without confirmation prompt');
|
||||
console.log(' update Check and update globally installed @git.zone packages');
|
||||
console.log(' update -y Update without confirmation prompt');
|
||||
console.log(' update --verbose Show detection diagnostics');
|
||||
console.log(' install Interactively install missing @git.zone packages');
|
||||
console.log(' install -y Install all missing packages without prompts');
|
||||
console.log(' install --verbose Show detection diagnostics');
|
||||
console.log('');
|
||||
console.log('Use "gtools <command> --help" for more information about a command.');
|
||||
});
|
||||
|
||||
toolsCli.addCommand('update').subscribe(async (argvArg) => {
|
||||
const yesFlag = argvArg.y === true || argvArg.yes === true;
|
||||
await modUpdate.run({ yes: yesFlag });
|
||||
const verboseFlag = argvArg.v === true || argvArg.verbose === true;
|
||||
await modUpdate.run({ yes: yesFlag, verbose: verboseFlag });
|
||||
});
|
||||
|
||||
toolsCli.addVersion('3.0.0');
|
||||
toolsCli.addCommand('install').subscribe(async (argvArg) => {
|
||||
const yesFlag = argvArg.y === true || argvArg.yes === true;
|
||||
const verboseFlag = argvArg.v === true || argvArg.verbose === true;
|
||||
await modInstall.run({ yes: yesFlag, verbose: verboseFlag });
|
||||
});
|
||||
|
||||
toolsCli.addVersion(commitinfo.version);
|
||||
toolsCli.startParse();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user