feat(cli): add machine-readable CLI help, recommendation, and configuration flows

This commit is contained in:
2026-04-16 18:54:07 +00:00
parent f43f88a3cb
commit fd7a73398c
14 changed files with 2482 additions and 786 deletions
+8
View File
@@ -1,5 +1,13 @@
# Changelog # Changelog
## 2026-04-16 - 2.14.0 - feat(cli)
add machine-readable CLI help, recommendation, and configuration flows
- introduces shared CLI mode handling for human, plain, and JSON output with configurable interactivity and update checks
- adds read-only JSON support for `commit recommend`, `format plan`, and command help output
- expands `config` and `services` commands with non-interactive config inspection and service enablement flows
- updates format and smartconfig handling to respect non-interactive execution and fail clearly when required metadata is missing
## 2026-04-16 - 2.13.16 - fix(mod_format) ## 2026-04-16 - 2.13.16 - fix(mod_format)
stop package.json formatter from modifying buildDocs and dependency entries stop package.json formatter from modifying buildDocs and dependency entries
+25 -32
View File
@@ -23,10 +23,10 @@ Gitzone CLI (`@git.zone/cli`) is a comprehensive toolbelt for streamlining local
### Configuration Management ### Configuration Management
- Uses `npmextra.json` for all tool configuration - Uses `.smartconfig.json` for tool configuration
- Configuration stored under `gitzone` key in npmextra - CLI settings live under the `@git.zone/cli` namespace
- No separate `.gitzonerc` file - everything in npmextra.json - Agent and non-interactive defaults now belong under `@git.zone/cli.cli`
- Project type and module metadata also stored in npmextra - Project type, module metadata, release settings, commit defaults, and format settings live in the same file
### Format Module (`mod_format`) - SIGNIFICANTLY ENHANCED ### Format Module (`mod_format`) - SIGNIFICANTLY ENHANCED
@@ -84,7 +84,7 @@ The format module is responsible for project standardization:
1. **Plan → Action Workflow**: Shows changes before applying them 1. **Plan → Action Workflow**: Shows changes before applying them
2. **Rollback Mechanism**: Full backup and restore on failures 2. **Rollback Mechanism**: Full backup and restore on failures
3. **Enhanced Configuration**: Granular control via npmextra.json 3. **Enhanced Configuration**: Granular control via `.smartconfig.json`
4. **Better Error Handling**: Detailed errors with recovery options 4. **Better Error Handling**: Detailed errors with recovery options
5. **Performance Optimizations**: Parallel execution and caching 5. **Performance Optimizations**: Parallel execution and caching
6. **Reporting**: Diff views, statistics, verbose logging 6. **Reporting**: Diff views, statistics, verbose logging
@@ -132,7 +132,7 @@ The commit module now supports `-y/--yes` flag for non-interactive commits:
## Development Tips ## Development Tips
- Always check readme.plan.md for ongoing improvement plans - Always check readme.plan.md for ongoing improvement plans
- Use npmextra.json for any new configuration options - Use `.smartconfig.json` for any new configuration options
- Keep modules focused and single-purpose - Keep modules focused and single-purpose
- Maintain the existing plugin pattern for dependencies - Maintain the existing plugin pattern for dependencies
- Test format operations on sample projects before deploying - Test format operations on sample projects before deploying
@@ -144,30 +144,18 @@ The commit module now supports `-y/--yes` flag for non-interactive commits:
```json ```json
{ {
"gitzone": { "@git.zone/cli": {
"cli": {
"interactive": true,
"output": "human",
"checkUpdates": true
},
"format": { "format": {
"interactive": true, "interactive": true,
"parallel": true,
"showStats": true, "showStats": true,
"cache": {
"enabled": true,
"clean": true
},
"rollback": {
"enabled": true,
"autoRollbackOnError": true,
"backupRetentionDays": 7
},
"modules": { "modules": {
"skip": ["prettier"], "skip": ["prettier"],
"only": [], "only": []
"order": []
},
"licenses": {
"allowed": ["MIT", "Apache-2.0"],
"exceptions": {
"some-package": "GPL-3.0"
}
} }
} }
} }
@@ -182,6 +170,9 @@ The commit module now supports `-y/--yes` flag for non-interactive commits:
# Interactive commit (default) # Interactive commit (default)
gitzone commit gitzone commit
# Read-only recommendation
gitzone commit recommend --json
# Auto-accept AI recommendations (no prompts) # Auto-accept AI recommendations (no prompts)
gitzone commit -y gitzone commit -y
gitzone commit --yes gitzone commit --yes
@@ -201,11 +192,14 @@ gitzone commit --format
# Basic format # Basic format
gitzone format gitzone format
# Read-only JSON plan
gitzone format plan --json
# Dry run to preview changes # Dry run to preview changes
gitzone format --dry-run gitzone format --dry-run
# Non-interactive mode # Non-interactive apply
gitzone format --yes gitzone format --write --yes
# Plan only (no execution) # Plan only (no execution)
gitzone format --plan-only gitzone format --plan-only
@@ -222,11 +216,10 @@ gitzone format --verbose
# Detailed diff views # Detailed diff views
gitzone format --detailed gitzone format --detailed
# Rollback operations # Inspect config for agents and scripts
gitzone format --rollback gitzone config show --json
gitzone format --rollback <operation-id> gitzone config set cli.output json
gitzone format --list-backups gitzone config get release.accessLevel
gitzone format --clean-backups
``` ```
## Common Issues (Now Resolved) ## Common Issues (Now Resolved)
+86 -64
View File
@@ -56,6 +56,9 @@ Create standardized commits with AI-powered suggestions that automatically handl
# Interactive commit with AI recommendations # Interactive commit with AI recommendations
gitzone commit gitzone commit
# Read-only recommendation for agents and scripts
gitzone commit recommend --json
# Auto-accept AI recommendations (skipped for BREAKING CHANGEs) # Auto-accept AI recommendations (skipped for BREAKING CHANGEs)
gitzone commit -y gitzone commit -y
@@ -65,14 +68,15 @@ gitzone commit -ypbr
**Flags:** **Flags:**
| Flag | Long Form | Description | | Flag | Long Form | Description |
|------|-----------|-------------| | ---- | ----------- | ---------------------------------------- |
| `-y` | `--yes` | Auto-accept AI recommendations | | `-y` | `--yes` | Auto-accept AI recommendations |
| `-p` | `--push` | Push to remote after commit | | `-p` | `--push` | Push to remote after commit |
| `-t` | `--test` | Run tests before committing | | `-t` | `--test` | Run tests before committing |
| `-b` | `--build` | Build after commit, verify clean tree | | `-b` | `--build` | Build after commit, verify clean tree |
| `-r` | `--release` | Publish to configured npm registries | | `-r` | `--release` | Publish to configured npm registries |
| | `--format` | Run format before committing | | | `--format` | Run format before committing |
| | `--json` | Emit JSON for `gitzone commit recommend` |
**Workflow steps:** **Workflow steps:**
@@ -94,6 +98,9 @@ Automatically format and standardize your entire codebase. **Dry-run by default*
# Preview what would change (default behavior) # Preview what would change (default behavior)
gitzone format gitzone format
# Emit a machine-readable plan
gitzone format plan --json
# Apply changes # Apply changes
gitzone format --write gitzone format --write
@@ -109,22 +116,22 @@ gitzone format --verbose
**Flags:** **Flags:**
| Flag | Description | | Flag | Description |
|------|-------------| | -------------------- | --------------------------------------------- |
| `--write` / `-w` | Apply changes (default is dry-run) | | `--write` / `-w` | Apply changes (default is dry-run) |
| `--yes` | Auto-approve without interactive confirmation | | `--yes` | Auto-approve without interactive confirmation |
| `--plan-only` | Only show what would be done | | `--plan-only` | Only show what would be done |
| `--save-plan <file>` | Save the format plan to a file | | `--save-plan <file>` | Save the format plan to a file |
| `--from-plan <file>` | Load and execute a saved plan | | `--from-plan <file>` | Load and execute a saved plan |
| `--detailed` | Show detailed stats and save report | | `--detailed` | Show detailed stats and save report |
| `--parallel` / `--no-parallel` | Toggle parallel execution | | `--verbose` | Enable verbose logging |
| `--verbose` | Enable verbose logging | | `--diff` | Show file diffs |
| `--diff` | Show file diffs | | `--json` | Emit a read-only format plan as JSON |
**Formatters (executed in order):** **Formatters (executed in order):**
1. 🧹 **Cleanup** — removes obsolete files (yarn.lock, package-lock.json, tslint.json, etc.) 1. 🧹 **Cleanup** — removes obsolete files (yarn.lock, package-lock.json, tslint.json, etc.)
2. ⚙️ **Npmextra** — formats and standardizes `npmextra.json` 2. ⚙️ **Smartconfig** — formats and standardizes `.smartconfig.json`
3. 📜 **License** — ensures proper licensing and checks dependency licenses 3. 📜 **License** — ensures proper licensing and checks dependency licenses
4. 📦 **Package.json** — standardizes package configuration 4. 📦 **Package.json** — standardizes package configuration
5. 📋 **Templates** — applies project template updates 5. 📋 **Templates** — applies project template updates
@@ -144,18 +151,21 @@ gitzone services [command]
**Commands:** **Commands:**
| Command | Description | | Command | Description |
|---------|-------------| | ------------------------ | ------------------------------------------------------ |
| `start [service]` | Start services (`mongo`\|`s3`\|`elasticsearch`\|`all`) | | `start [service]` | Start services (`mongo`\|`s3`\|`elasticsearch`\|`all`) |
| `stop [service]` | Stop services | | `stop [service]` | Stop services |
| `restart [service]` | Restart services | | `restart [service]` | Restart services |
| `status` | Show current service status | | `status` | Show current service status |
| `config` | Display configuration details | | `config` | Display configuration details |
| `compass` | Get MongoDB Compass connection string with network IP | | `set <csv>` | Set enabled services without prompts |
| `logs [service] [lines]` | View service logs (default: 20 lines) | | `enable <service...>` | Enable one or more services |
| `reconfigure` | Reassign ports and restart all services | | `disable <service...>` | Disable one or more services |
| `remove` | Remove containers (preserves data) | | `compass` | Get MongoDB Compass connection string with network IP |
| `clean` | Remove containers AND data (⚠️ destructive) | | `logs [service] [lines]` | View service logs (default: 20 lines) |
| `reconfigure` | Reassign ports and restart all services |
| `remove` | Remove containers (preserves data) |
| `clean` | Remove containers AND data (⚠️ destructive) |
**Service aliases:** **Service aliases:**
@@ -195,6 +205,9 @@ gitzone services cleanup -g
# Start all services for your project # Start all services for your project
gitzone services start gitzone services start
# Configure enabled services without prompts
gitzone services set mongodb,minio
# Check what's running # Check what's running
gitzone services status gitzone services status
@@ -217,18 +230,21 @@ Manage release registries and commit settings:
gitzone config [subcommand] gitzone config [subcommand]
``` ```
| Command | Description | | Command | Description |
|---------|-------------| | ---------------------------------- | ---------------------------------------------------------- |
| `show` | Display current release config (registries, access level) | | `show` | Display current release config (registries, access level) |
| `add [url]` | Add a registry URL (default: `https://registry.npmjs.org`) | | `get <path>` | Read a single value from `@git.zone/cli` |
| `remove [url]` | Remove a registry URL (interactive selection if no URL) | | `set <path> <value>` | Write a single value to `@git.zone/cli` |
| `clear` | Clear all registries (with confirmation) | | `unset <path>` | Remove a single value from `@git.zone/cli` |
| `access [public\|private]` | Set npm access level for publishing | | `add [url]` | Add a registry URL (default: `https://registry.npmjs.org`) |
| `commit alwaysTest [true\|false]` | Always run tests before commit | | `remove [url]` | Remove a registry URL (interactive selection if no URL) |
| `commit alwaysBuild [true\|false]` | Always build after commit | | `clear` | Clear all registries (with confirmation) |
| `services` | Configure which services are enabled | | `access [public\|private]` | Set npm access level for publishing |
| `commit alwaysTest [true\|false]` | Always run tests before commit |
| `commit alwaysBuild [true\|false]` | Always build after commit |
| `services` | Configure which services are enabled |
Configuration is stored in `npmextra.json` under the `@git.zone/cli` key. Configuration is stored in `.smartconfig.json` under the `@git.zone/cli` key.
### 📦 Project Templates ### 📦 Project Templates
@@ -323,44 +339,33 @@ gitzone helpers shortid
## 📋 Configuration ## 📋 Configuration
### npmextra.json ### .smartconfig.json
Customize gitzone behavior through `npmextra.json`: Customize gitzone behavior through `.smartconfig.json`:
```json ```json
{ {
"@git.zone/cli": { "@git.zone/cli": {
"projectType": "npm", "projectType": "npm",
"cli": {
"interactive": true,
"output": "human",
"checkUpdates": true
},
"release": { "release": {
"registries": [ "registries": ["https://registry.npmjs.org"],
"https://registry.npmjs.org"
],
"accessLevel": "public" "accessLevel": "public"
}, },
"commit": { "commit": {
"alwaysTest": false, "alwaysTest": false,
"alwaysBuild": false "alwaysBuild": false
} },
},
"gitzone": {
"format": { "format": {
"interactive": true, "interactive": true,
"parallel": true,
"showStats": true, "showStats": true,
"cache": {
"enabled": true,
"clean": true
},
"modules": { "modules": {
"skip": ["prettier"], "skip": ["prettier"],
"only": [], "only": []
"order": []
},
"licenses": {
"allowed": ["MIT", "Apache-2.0"],
"exceptions": {
"some-package": "GPL-3.0"
}
} }
} }
} }
@@ -408,6 +413,23 @@ gitzone services stop
gitzone commit -ytbpr gitzone commit -ytbpr
``` ```
### Agent-Friendly Inspection
```bash
# Top-level machine-readable help
gitzone help config --json
# Read-only commit recommendation
gitzone commit recommend --json
# Read-only format plan
gitzone format plan --json
# Read or change config without prompts
gitzone config get release.accessLevel
gitzone config set cli.interactive false
```
### Multi-Repository Management ### Multi-Repository Management
```bash ```bash
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/cli', name: '@git.zone/cli',
version: '2.13.16', version: '2.14.0',
description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.' description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
} }
+50 -38
View File
@@ -1,23 +1,29 @@
import * as plugins from './plugins.js'; import * as plugins from "./plugins.js";
import * as paths from './paths.js'; import * as paths from "./paths.js";
import { GitzoneConfig } from './classes.gitzoneconfig.js'; import { GitzoneConfig } from "./classes.gitzoneconfig.js";
import { getRawCliMode } from "./helpers.climode.js";
const gitzoneSmartcli = new plugins.smartcli.Smartcli(); const gitzoneSmartcli = new plugins.smartcli.Smartcli();
export let run = async () => { export let run = async () => {
const done = plugins.smartpromise.defer(); const done = plugins.smartpromise.defer();
const rawCliMode = await getRawCliMode();
// get packageInfo // get packageInfo
const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir); const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
// check for updates // check for updates
const smartupdateInstance = new plugins.smartupdate.SmartUpdate(); if (rawCliMode.checkUpdates) {
await smartupdateInstance.check( const smartupdateInstance = new plugins.smartupdate.SmartUpdate();
'gitzone', await smartupdateInstance.check(
projectInfo.npm.version, "gitzone",
'http://gitzone.gitlab.io/gitzone/changelog.html', projectInfo.npm.version,
); "http://gitzone.gitlab.io/gitzone/changelog.html",
console.log('---------------------------------------------'); );
}
if (rawCliMode.output === "human") {
console.log("---------------------------------------------");
}
gitzoneSmartcli.addVersion(projectInfo.npm.version); gitzoneSmartcli.addVersion(projectInfo.npm.version);
// ======> Standard task <====== // ======> Standard task <======
@@ -26,8 +32,13 @@ export let run = async () => {
* standard task * standard task
*/ */
gitzoneSmartcli.standardCommand().subscribe(async (argvArg) => { gitzoneSmartcli.standardCommand().subscribe(async (argvArg) => {
const modStandard = await import('./mod_standard/index.js'); const modStandard = await import("./mod_standard/index.js");
await modStandard.run(); await modStandard.run(argvArg);
});
gitzoneSmartcli.addCommand("help").subscribe(async (argvArg) => {
const modStandard = await import("./mod_standard/index.js");
await modStandard.run(argvArg);
}); });
// ======> Specific tasks <====== // ======> Specific tasks <======
@@ -35,43 +46,44 @@ export let run = async () => {
/** /**
* commit something * commit something
*/ */
gitzoneSmartcli.addCommand('commit').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("commit").subscribe(async (argvArg) => {
const modCommit = await import('./mod_commit/index.js'); const modCommit = await import("./mod_commit/index.js");
await modCommit.run(argvArg); await modCommit.run(argvArg);
}); });
/** /**
* deprecate a package on npm * deprecate a package on npm
*/ */
gitzoneSmartcli.addCommand('deprecate').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("deprecate").subscribe(async (argvArg) => {
const modDeprecate = await import('./mod_deprecate/index.js'); const modDeprecate = await import("./mod_deprecate/index.js");
await modDeprecate.run(); await modDeprecate.run();
}); });
/** /**
* docker * docker
*/ */
gitzoneSmartcli.addCommand('docker').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("docker").subscribe(async (argvArg) => {
const modDocker = await import('./mod_docker/index.js'); const modDocker = await import("./mod_docker/index.js");
await modDocker.run(argvArg); await modDocker.run(argvArg);
}); });
/** /**
* Update all files that comply with the gitzone standard * Update all files that comply with the gitzone standard
*/ */
gitzoneSmartcli.addCommand('format').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("format").subscribe(async (argvArg) => {
const config = GitzoneConfig.fromCwd(); const config = GitzoneConfig.fromCwd();
const modFormat = await import('./mod_format/index.js'); const modFormat = await import("./mod_format/index.js");
// Handle format with options // Handle format with options
// Default is dry-mode, use --write/-w to apply changes // Default is dry-mode, use --write/-w to apply changes
await modFormat.run({ await modFormat.run({
...argvArg,
write: argvArg.write || argvArg.w, write: argvArg.write || argvArg.w,
dryRun: argvArg['dry-run'], dryRun: argvArg["dry-run"],
yes: argvArg.yes, yes: argvArg.yes,
planOnly: argvArg['plan-only'], planOnly: argvArg["plan-only"],
savePlan: argvArg['save-plan'], savePlan: argvArg["save-plan"],
fromPlan: argvArg['from-plan'], fromPlan: argvArg["from-plan"],
detailed: argvArg.detailed, detailed: argvArg.detailed,
interactive: argvArg.interactive !== false, interactive: argvArg.interactive !== false,
verbose: argvArg.verbose, verbose: argvArg.verbose,
@@ -82,54 +94,54 @@ export let run = async () => {
/** /**
* run meta commands * run meta commands
*/ */
gitzoneSmartcli.addCommand('meta').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("meta").subscribe(async (argvArg) => {
const config = GitzoneConfig.fromCwd(); const config = GitzoneConfig.fromCwd();
const modMeta = await import('./mod_meta/index.js'); const modMeta = await import("./mod_meta/index.js");
modMeta.run(argvArg); modMeta.run(argvArg);
}); });
/** /**
* open assets * open assets
*/ */
gitzoneSmartcli.addCommand('open').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("open").subscribe(async (argvArg) => {
const modOpen = await import('./mod_open/index.js'); const modOpen = await import("./mod_open/index.js");
modOpen.run(argvArg); modOpen.run(argvArg);
}); });
/** /**
* add a readme to a project * add a readme to a project
*/ */
gitzoneSmartcli.addCommand('template').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("template").subscribe(async (argvArg) => {
const modTemplate = await import('./mod_template/index.js'); const modTemplate = await import("./mod_template/index.js");
modTemplate.run(argvArg); modTemplate.run(argvArg);
}); });
/** /**
* start working on a project * start working on a project
*/ */
gitzoneSmartcli.addCommand('start').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("start").subscribe(async (argvArg) => {
const modTemplate = await import('./mod_start/index.js'); const modTemplate = await import("./mod_start/index.js");
modTemplate.run(argvArg); modTemplate.run(argvArg);
}); });
gitzoneSmartcli.addCommand('helpers').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("helpers").subscribe(async (argvArg) => {
const modHelpers = await import('./mod_helpers/index.js'); const modHelpers = await import("./mod_helpers/index.js");
modHelpers.run(argvArg); modHelpers.run(argvArg);
}); });
/** /**
* manage release configuration * manage release configuration
*/ */
gitzoneSmartcli.addCommand('config').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("config").subscribe(async (argvArg) => {
const modConfig = await import('./mod_config/index.js'); const modConfig = await import("./mod_config/index.js");
await modConfig.run(argvArg); await modConfig.run(argvArg);
}); });
/** /**
* manage development services (MongoDB, S3/MinIO) * manage development services (MongoDB, S3/MinIO)
*/ */
gitzoneSmartcli.addCommand('services').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("services").subscribe(async (argvArg) => {
const modServices = await import('./mod_services/index.js'); const modServices = await import("./mod_services/index.js");
await modServices.run(argvArg); await modServices.run(argvArg);
}); });
+212
View File
@@ -0,0 +1,212 @@
import { getCliConfigValue } from "./helpers.smartconfig.js";
export type TCliOutputMode = "human" | "plain" | "json";
export interface ICliMode {
output: TCliOutputMode;
interactive: boolean;
json: boolean;
plain: boolean;
quiet: boolean;
yes: boolean;
help: boolean;
agent: boolean;
checkUpdates: boolean;
isTty: boolean;
command?: string;
}
interface ICliConfigSettings {
interactive?: boolean;
output?: TCliOutputMode;
checkUpdates?: boolean;
}
type TArgSource = Record<string, any> & { _?: string[] };
const camelCase = (value: string): string => {
return value.replace(/-([a-z])/g, (_match, group: string) =>
group.toUpperCase(),
);
};
const getArgValue = (argvArg: TArgSource, key: string): any => {
const keyVariants = [key, camelCase(key), key.replace(/-/g, "")];
for (const keyVariant of keyVariants) {
if (argvArg[keyVariant] !== undefined) {
return argvArg[keyVariant];
}
}
return undefined;
};
const parseRawArgv = (argv: string[]): TArgSource => {
const parsedArgv: TArgSource = { _: [] };
for (let i = 0; i < argv.length; i++) {
const currentArg = argv[i];
if (currentArg.startsWith("--no-")) {
const key = currentArg.slice(5);
parsedArgv[key] = false;
parsedArgv[camelCase(key)] = false;
continue;
}
if (currentArg.startsWith("--")) {
const withoutPrefix = currentArg.slice(2);
const [rawKey, inlineValue] = withoutPrefix.split("=", 2);
if (inlineValue !== undefined) {
parsedArgv[rawKey] = inlineValue;
parsedArgv[camelCase(rawKey)] = inlineValue;
continue;
}
const nextArg = argv[i + 1];
if (nextArg && !nextArg.startsWith("-")) {
parsedArgv[rawKey] = nextArg;
parsedArgv[camelCase(rawKey)] = nextArg;
i++;
} else {
parsedArgv[rawKey] = true;
parsedArgv[camelCase(rawKey)] = true;
}
continue;
}
if (currentArg.startsWith("-") && currentArg.length > 1) {
for (const shortFlag of currentArg.slice(1).split("")) {
parsedArgv[shortFlag] = true;
}
continue;
}
parsedArgv._ = parsedArgv._ || [];
parsedArgv._.push(currentArg);
}
return parsedArgv;
};
const normalizeOutputMode = (value: unknown): TCliOutputMode | undefined => {
if (value === "human" || value === "plain" || value === "json") {
return value;
}
return undefined;
};
const resolveCliMode = (
argvArg: TArgSource,
cliConfig: ICliConfigSettings,
): ICliMode => {
const isTty = Boolean(process.stdout?.isTTY && process.stdin?.isTTY);
const agentMode = Boolean(getArgValue(argvArg, "agent"));
const outputOverride = normalizeOutputMode(getArgValue(argvArg, "output"));
let output: TCliOutputMode =
normalizeOutputMode(cliConfig.output) || (isTty ? "human" : "plain");
if (agentMode || getArgValue(argvArg, "json")) {
output = "json";
} else if (getArgValue(argvArg, "plain")) {
output = "plain";
} else if (outputOverride) {
output = outputOverride;
}
const interactiveSetting = getArgValue(argvArg, "interactive");
let interactive = cliConfig.interactive ?? isTty;
if (interactiveSetting === true) {
interactive = true;
} else if (interactiveSetting === false) {
interactive = false;
}
if (!isTty || output !== "human" || agentMode) {
interactive = false;
}
const checkUpdatesSetting = getArgValue(argvArg, "check-updates");
let checkUpdates = cliConfig.checkUpdates ?? output === "human";
if (checkUpdatesSetting === true) {
checkUpdates = true;
} else if (checkUpdatesSetting === false) {
checkUpdates = false;
}
if (output !== "human" || agentMode) {
checkUpdates = false;
}
return {
output,
interactive,
json: output === "json",
plain: output === "plain",
quiet: Boolean(
getArgValue(argvArg, "quiet") ||
getArgValue(argvArg, "q") ||
output === "json",
),
yes: Boolean(getArgValue(argvArg, "yes") || getArgValue(argvArg, "y")),
help: Boolean(
getArgValue(argvArg, "help") ||
getArgValue(argvArg, "h") ||
argvArg._?.[0] === "help",
),
agent: agentMode,
checkUpdates,
isTty,
command: argvArg._?.[0],
};
};
const getCliModeConfig = async (): Promise<ICliConfigSettings> => {
return await getCliConfigValue<ICliConfigSettings>("cli", {});
};
export const getCliMode = async (
argvArg: TArgSource = {},
): Promise<ICliMode> => {
const cliConfig = await getCliModeConfig();
return resolveCliMode(argvArg, cliConfig);
};
export const getRawCliMode = async (): Promise<ICliMode> => {
const cliConfig = await getCliModeConfig();
const rawArgv = parseRawArgv(process.argv.slice(2));
return resolveCliMode(rawArgv, cliConfig);
};
export const printJson = (data: unknown): void => {
console.log(JSON.stringify(data, null, 2));
};
export const runWithSuppressedOutput = async <T>(
fn: () => Promise<T>,
): Promise<T> => {
const originalConsole = {
log: console.log,
info: console.info,
warn: console.warn,
error: console.error,
};
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
const originalStderrWrite = process.stderr.write.bind(process.stderr);
const noop = () => undefined;
console.log = noop;
console.info = noop;
console.warn = noop;
console.error = noop;
process.stdout.write = (() => true) as typeof process.stdout.write;
process.stderr.write = (() => true) as typeof process.stderr.write;
try {
return await fn();
} finally {
console.log = originalConsole.log;
console.info = originalConsole.info;
console.warn = originalConsole.warn;
console.error = originalConsole.error;
process.stdout.write = originalStdoutWrite;
process.stderr.write = originalStderrWrite;
}
};
+192
View File
@@ -0,0 +1,192 @@
import * as plugins from "./plugins.js";
import { rename, writeFile } from "fs/promises";
export const CLI_NAMESPACE = "@git.zone/cli";
const isPlainObject = (value: unknown): value is Record<string, any> => {
return typeof value === "object" && value !== null && !Array.isArray(value);
};
export const getSmartconfigPath = (cwd: string = process.cwd()): string => {
return plugins.path.join(cwd, ".smartconfig.json");
};
export const readSmartconfigFile = async (
cwd: string = process.cwd(),
): Promise<Record<string, any>> => {
const smartconfigPath = getSmartconfigPath(cwd);
if (!(await plugins.smartfs.file(smartconfigPath).exists())) {
return {};
}
const content = (await plugins.smartfs
.file(smartconfigPath)
.encoding("utf8")
.read()) as string;
if (content.trim() === "") {
return {};
}
return JSON.parse(content);
};
export const writeSmartconfigFile = async (
data: Record<string, any>,
cwd: string = process.cwd(),
): Promise<void> => {
const smartconfigPath = getSmartconfigPath(cwd);
const tempPath = `${smartconfigPath}.tmp-${Date.now()}`;
const content = JSON.stringify(data, null, 2);
await writeFile(tempPath, content, "utf8");
await rename(tempPath, smartconfigPath);
};
export const normalizeCliConfigPath = (configPath: string): string => {
const trimmedPath = configPath.trim();
if (!trimmedPath || trimmedPath === CLI_NAMESPACE) {
return "";
}
if (trimmedPath.startsWith(`${CLI_NAMESPACE}.`)) {
return trimmedPath.slice(`${CLI_NAMESPACE}.`.length);
}
return trimmedPath;
};
export const getCliConfigPathSegments = (configPath: string): string[] => {
const normalizedPath = normalizeCliConfigPath(configPath);
if (!normalizedPath) {
return [];
}
return normalizedPath
.split(".")
.map((segment) => segment.trim())
.filter(Boolean);
};
export const getCliNamespaceConfig = (
smartconfigData: Record<string, any>,
): Record<string, any> => {
const cliConfig = smartconfigData[CLI_NAMESPACE];
if (isPlainObject(cliConfig)) {
return cliConfig;
}
return {};
};
export const getCliConfigValueFromData = (
smartconfigData: Record<string, any>,
configPath: string,
): any => {
const segments = getCliConfigPathSegments(configPath);
let currentValue: any = getCliNamespaceConfig(smartconfigData);
for (const segment of segments) {
if (!isPlainObject(currentValue) && !Array.isArray(currentValue)) {
return undefined;
}
currentValue = (currentValue as any)?.[segment];
}
return currentValue;
};
export const getCliConfigValue = async <T>(
configPath: string,
defaultValue: T,
cwd: string = process.cwd(),
): Promise<T> => {
const smartconfigData = await readSmartconfigFile(cwd);
const configValue = getCliConfigValueFromData(smartconfigData, configPath);
if (configValue === undefined) {
return defaultValue;
}
if (isPlainObject(defaultValue) && isPlainObject(configValue)) {
return {
...defaultValue,
...configValue,
} as T;
}
return configValue as T;
};
export const setCliConfigValueInData = (
smartconfigData: Record<string, any>,
configPath: string,
value: any,
): Record<string, any> => {
const segments = getCliConfigPathSegments(configPath);
if (!isPlainObject(smartconfigData[CLI_NAMESPACE])) {
smartconfigData[CLI_NAMESPACE] = {};
}
if (segments.length === 0) {
smartconfigData[CLI_NAMESPACE] = value;
return smartconfigData;
}
let currentValue = smartconfigData[CLI_NAMESPACE];
for (const segment of segments.slice(0, -1)) {
if (!isPlainObject(currentValue[segment])) {
currentValue[segment] = {};
}
currentValue = currentValue[segment];
}
currentValue[segments[segments.length - 1]] = value;
return smartconfigData;
};
export const unsetCliConfigValueInData = (
smartconfigData: Record<string, any>,
configPath: string,
): boolean => {
const segments = getCliConfigPathSegments(configPath);
if (segments.length === 0) {
if (smartconfigData[CLI_NAMESPACE] !== undefined) {
delete smartconfigData[CLI_NAMESPACE];
return true;
}
return false;
}
const parentSegments = segments.slice(0, -1);
let currentValue: any = getCliNamespaceConfig(smartconfigData);
const objectPath: Array<Record<string, any>> = [currentValue];
for (const segment of parentSegments) {
if (!isPlainObject(currentValue[segment])) {
return false;
}
currentValue = currentValue[segment];
objectPath.push(currentValue);
}
const lastSegment = segments[segments.length - 1];
if (!(lastSegment in currentValue)) {
return false;
}
delete currentValue[lastSegment];
for (let i = objectPath.length - 1; i >= 1; i--) {
if (Object.keys(objectPath[i]).length > 0) {
break;
}
const parentObject = objectPath[i - 1];
const parentKey = parentSegments[i - 1];
delete parentObject[parentKey];
}
if (Object.keys(getCliNamespaceConfig(smartconfigData)).length === 0) {
delete smartconfigData[CLI_NAMESPACE];
}
return true;
};
+325 -98
View File
@@ -1,13 +1,41 @@
// this file contains code to create commits in a consistent way // this file contains code to create commits in a consistent way
import * as plugins from './mod.plugins.js'; import * as plugins from "./mod.plugins.js";
import * as paths from '../paths.js'; import * as paths from "../paths.js";
import { logger } from '../gitzone.logging.js'; import { logger } from "../gitzone.logging.js";
import * as helpers from './mod.helpers.js'; import * as helpers from "./mod.helpers.js";
import * as ui from './mod.ui.js'; import * as ui from "./mod.ui.js";
import { ReleaseConfig } from '../mod_config/classes.releaseconfig.js'; import { ReleaseConfig } from "../mod_config/classes.releaseconfig.js";
import type { ICliMode } from "../helpers.climode.js";
import {
getCliMode,
printJson,
runWithSuppressedOutput,
} from "../helpers.climode.js";
export const run = async (argvArg: any) => { export const run = async (argvArg: any) => {
const mode = await getCliMode(argvArg);
const subcommand = argvArg._?.[1];
if (mode.help || subcommand === "help") {
showHelp(mode);
return;
}
if (subcommand === "recommend") {
await handleRecommend(mode);
return;
}
if (mode.json) {
printJson({
ok: false,
error:
"JSON output is only supported for the read-only recommendation flow. Use `gitzone commit recommend --json`.",
});
return;
}
// Read commit config from .smartconfig.json // Read commit config from .smartconfig.json
const smartconfigInstance = new plugins.smartconfig.Smartconfig(); const smartconfigInstance = new plugins.smartconfig.Smartconfig();
const gitzoneConfig = smartconfigInstance.dataFor<{ const gitzoneConfig = smartconfigInstance.dataFor<{
@@ -15,7 +43,7 @@ export const run = async (argvArg: any) => {
alwaysTest?: boolean; alwaysTest?: boolean;
alwaysBuild?: boolean; alwaysBuild?: boolean;
}; };
}>('@git.zone/cli', {}); }>("@git.zone/cli", {});
const commitConfig = gitzoneConfig.commit || {}; const commitConfig = gitzoneConfig.commit || {};
// Check flags and merge with config options // Check flags and merge with config options
@@ -27,10 +55,12 @@ export const run = async (argvArg: any) => {
if (wantsRelease) { if (wantsRelease) {
releaseConfig = await ReleaseConfig.fromCwd(); releaseConfig = await ReleaseConfig.fromCwd();
if (!releaseConfig.hasRegistries()) { if (!releaseConfig.hasRegistries()) {
logger.log('error', 'No release registries configured.'); logger.log("error", "No release registries configured.");
console.log(''); console.log("");
console.log(' Run `gitzone config add <registry-url>` to add registries.'); console.log(
console.log(''); " Run `gitzone config add <registry-url>` to add registries.",
);
console.log("");
process.exit(1); process.exit(1);
} }
} }
@@ -47,26 +77,26 @@ export const run = async (argvArg: any) => {
}); });
if (argvArg.format) { if (argvArg.format) {
const formatMod = await import('../mod_format/index.js'); const formatMod = await import("../mod_format/index.js");
await formatMod.run(); await formatMod.run();
} }
// Run tests early to fail fast before analysis // Run tests early to fail fast before analysis
if (wantsTest) { if (wantsTest) {
ui.printHeader('🧪 Running tests...'); ui.printHeader("🧪 Running tests...");
const smartshellForTest = new plugins.smartshell.Smartshell({ const smartshellForTest = new plugins.smartshell.Smartshell({
executor: 'bash', executor: "bash",
sourceFilePaths: [], sourceFilePaths: [],
}); });
const testResult = await smartshellForTest.exec('pnpm test'); const testResult = await smartshellForTest.exec("pnpm test");
if (testResult.exitCode !== 0) { if (testResult.exitCode !== 0) {
logger.log('error', 'Tests failed. Aborting commit.'); logger.log("error", "Tests failed. Aborting commit.");
process.exit(1); process.exit(1);
} }
logger.log('success', 'All tests passed.'); logger.log("success", "All tests passed.");
} }
ui.printHeader('🔍 Analyzing repository changes...'); ui.printHeader("🔍 Analyzing repository changes...");
const aidoc = new plugins.tsdoc.AiDoc(); const aidoc = new plugins.tsdoc.AiDoc();
await aidoc.start(); await aidoc.start();
@@ -79,58 +109,63 @@ export const run = async (argvArg: any) => {
recommendedNextVersion: nextCommitObject.recommendedNextVersion, recommendedNextVersion: nextCommitObject.recommendedNextVersion,
recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel, recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel,
recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope, recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope,
recommendedNextVersionMessage: nextCommitObject.recommendedNextVersionMessage, recommendedNextVersionMessage:
nextCommitObject.recommendedNextVersionMessage,
}); });
let answerBucket: plugins.smartinteract.AnswerBucket; let answerBucket: plugins.smartinteract.AnswerBucket;
// Check if -y/--yes flag is set AND version is not a breaking change // Check if -y/--yes flag is set AND version is not a breaking change
// Breaking changes (major version bumps) always require manual confirmation // Breaking changes (major version bumps) always require manual confirmation
const isBreakingChange = nextCommitObject.recommendedNextVersionLevel === 'BREAKING CHANGE'; const isBreakingChange =
nextCommitObject.recommendedNextVersionLevel === "BREAKING CHANGE";
const canAutoAccept = (argvArg.y || argvArg.yes) && !isBreakingChange; const canAutoAccept = (argvArg.y || argvArg.yes) && !isBreakingChange;
if (canAutoAccept) { if (canAutoAccept) {
// Auto-mode: create AnswerBucket programmatically // Auto-mode: create AnswerBucket programmatically
logger.log('info', '✓ Auto-accepting AI recommendations (--yes flag)'); logger.log("info", "✓ Auto-accepting AI recommendations (--yes flag)");
answerBucket = new plugins.smartinteract.AnswerBucket(); answerBucket = new plugins.smartinteract.AnswerBucket();
answerBucket.addAnswer({ answerBucket.addAnswer({
name: 'commitType', name: "commitType",
value: nextCommitObject.recommendedNextVersionLevel, value: nextCommitObject.recommendedNextVersionLevel,
}); });
answerBucket.addAnswer({ answerBucket.addAnswer({
name: 'commitScope', name: "commitScope",
value: nextCommitObject.recommendedNextVersionScope, value: nextCommitObject.recommendedNextVersionScope,
}); });
answerBucket.addAnswer({ answerBucket.addAnswer({
name: 'commitDescription', name: "commitDescription",
value: nextCommitObject.recommendedNextVersionMessage, value: nextCommitObject.recommendedNextVersionMessage,
}); });
answerBucket.addAnswer({ answerBucket.addAnswer({
name: 'pushToOrigin', name: "pushToOrigin",
value: !!(argvArg.p || argvArg.push), // Only push if -p flag also provided value: !!(argvArg.p || argvArg.push), // Only push if -p flag also provided
}); });
answerBucket.addAnswer({ answerBucket.addAnswer({
name: 'createRelease', name: "createRelease",
value: wantsRelease, value: wantsRelease,
}); });
} else { } else {
// Warn if --yes was provided but we're requiring confirmation due to breaking change // Warn if --yes was provided but we're requiring confirmation due to breaking change
if (isBreakingChange && (argvArg.y || argvArg.yes)) { if (isBreakingChange && (argvArg.y || argvArg.yes)) {
logger.log('warn', '⚠️ BREAKING CHANGE detected - manual confirmation required'); logger.log(
"warn",
"⚠️ BREAKING CHANGE detected - manual confirmation required",
);
} }
// Interactive mode: prompt user for input // Interactive mode: prompt user for input
const commitInteract = new plugins.smartinteract.SmartInteract(); const commitInteract = new plugins.smartinteract.SmartInteract();
commitInteract.addQuestions([ commitInteract.addQuestions([
{ {
type: 'list', type: "list",
name: `commitType`, name: `commitType`,
message: `Choose TYPE of the commit:`, message: `Choose TYPE of the commit:`,
choices: [`fix`, `feat`, `BREAKING CHANGE`], choices: [`fix`, `feat`, `BREAKING CHANGE`],
default: nextCommitObject.recommendedNextVersionLevel, default: nextCommitObject.recommendedNextVersionLevel,
}, },
{ {
type: 'input', type: "input",
name: `commitScope`, name: `commitScope`,
message: `What is the SCOPE of the commit:`, message: `What is the SCOPE of the commit:`,
default: nextCommitObject.recommendedNextVersionScope, default: nextCommitObject.recommendedNextVersionScope,
@@ -142,13 +177,13 @@ export const run = async (argvArg: any) => {
default: nextCommitObject.recommendedNextVersionMessage, default: nextCommitObject.recommendedNextVersionMessage,
}, },
{ {
type: 'confirm', type: "confirm",
name: `pushToOrigin`, name: `pushToOrigin`,
message: `Do you want to push this version now?`, message: `Do you want to push this version now?`,
default: true, default: true,
}, },
{ {
type: 'confirm', type: "confirm",
name: `createRelease`, name: `createRelease`,
message: `Do you want to publish to npm registries?`, message: `Do you want to publish to npm registries?`,
default: wantsRelease, default: wantsRelease,
@@ -157,40 +192,50 @@ export const run = async (argvArg: any) => {
answerBucket = await commitInteract.runQueue(); answerBucket = await commitInteract.runQueue();
} }
const commitString = createCommitStringFromAnswerBucket(answerBucket); const commitString = createCommitStringFromAnswerBucket(answerBucket);
const commitVersionType = (() => { const commitType = answerBucket.getAnswerFor("commitType");
switch (answerBucket.getAnswerFor('commitType')) { let commitVersionType: helpers.VersionType;
case 'fix': switch (commitType) {
return 'patch'; case "fix":
case 'feat': commitVersionType = "patch";
return 'minor'; break;
case 'BREAKING CHANGE': case "feat":
return 'major'; commitVersionType = "minor";
} break;
})(); case "BREAKING CHANGE":
commitVersionType = "major";
break;
default:
throw new Error(`Unsupported commit type: ${commitType}`);
}
ui.printHeader('✨ Creating Semantic Commit'); ui.printHeader("✨ Creating Semantic Commit");
ui.printCommitMessage(commitString); ui.printCommitMessage(commitString);
const smartshellInstance = new plugins.smartshell.Smartshell({ const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash', executor: "bash",
sourceFilePaths: [], sourceFilePaths: [],
}); });
// Load release config if user wants to release (interactively selected) // Load release config if user wants to release (interactively selected)
if (answerBucket.getAnswerFor('createRelease') && !releaseConfig) { if (answerBucket.getAnswerFor("createRelease") && !releaseConfig) {
releaseConfig = await ReleaseConfig.fromCwd(); releaseConfig = await ReleaseConfig.fromCwd();
if (!releaseConfig.hasRegistries()) { if (!releaseConfig.hasRegistries()) {
logger.log('error', 'No release registries configured.'); logger.log("error", "No release registries configured.");
console.log(''); console.log("");
console.log(' Run `gitzone config add <registry-url>` to add registries.'); console.log(
console.log(''); " Run `gitzone config add <registry-url>` to add registries.",
);
console.log("");
process.exit(1); process.exit(1);
} }
} }
// Determine total steps based on options // Determine total steps based on options
// Note: test runs early (like format) so not counted in numbered steps // Note: test runs early (like format) so not counted in numbered steps
const willPush = answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true'); const willPush =
const willRelease = answerBucket.getAnswerFor('createRelease') && releaseConfig?.hasRegistries(); answerBucket.getAnswerFor("pushToOrigin") && !(process.env.CI === "true");
const willRelease =
answerBucket.getAnswerFor("createRelease") &&
releaseConfig?.hasRegistries();
let totalSteps = 5; // Base steps: commitinfo, changelog, staging, commit, version let totalSteps = 5; // Base steps: commitinfo, changelog, staging, commit, version
if (wantsBuild) totalSteps += 2; // build step + verification step if (wantsBuild) totalSteps += 2; // build step + verification step
if (willPush) totalSteps++; if (willPush) totalSteps++;
@@ -199,96 +244,156 @@ export const run = async (argvArg: any) => {
// Step 1: Baking commitinfo // Step 1: Baking commitinfo
currentStep++; currentStep++;
ui.printStep(currentStep, totalSteps, '🔧 Baking commit info into code', 'in-progress'); ui.printStep(
currentStep,
totalSteps,
"🔧 Baking commit info into code",
"in-progress",
);
const commitInfo = new plugins.commitinfo.CommitInfo( const commitInfo = new plugins.commitinfo.CommitInfo(
paths.cwd, paths.cwd,
commitVersionType, commitVersionType,
); );
await commitInfo.writeIntoPotentialDirs(); await commitInfo.writeIntoPotentialDirs();
ui.printStep(currentStep, totalSteps, '🔧 Baking commit info into code', 'done'); ui.printStep(
currentStep,
totalSteps,
"🔧 Baking commit info into code",
"done",
);
// Step 2: Writing changelog // Step 2: Writing changelog
currentStep++; currentStep++;
ui.printStep(currentStep, totalSteps, '📄 Generating changelog.md', 'in-progress'); ui.printStep(
let changelog = nextCommitObject.changelog; currentStep,
totalSteps,
"📄 Generating changelog.md",
"in-progress",
);
let changelog = nextCommitObject.changelog || "# Changelog\n";
changelog = changelog.replaceAll( changelog = changelog.replaceAll(
'{{nextVersion}}', "{{nextVersion}}",
(await commitInfo.getNextPlannedVersion()).versionString, (await commitInfo.getNextPlannedVersion()).versionString,
); );
changelog = changelog.replaceAll( changelog = changelog.replaceAll(
'{{nextVersionScope}}', "{{nextVersionScope}}",
`${await answerBucket.getAnswerFor('commitType')}(${await answerBucket.getAnswerFor('commitScope')})`, `${await answerBucket.getAnswerFor("commitType")}(${await answerBucket.getAnswerFor("commitScope")})`,
); );
changelog = changelog.replaceAll( changelog = changelog.replaceAll(
'{{nextVersionMessage}}', "{{nextVersionMessage}}",
nextCommitObject.recommendedNextVersionMessage, nextCommitObject.recommendedNextVersionMessage,
); );
if (nextCommitObject.recommendedNextVersionDetails?.length > 0) { if (nextCommitObject.recommendedNextVersionDetails?.length > 0) {
changelog = changelog.replaceAll( changelog = changelog.replaceAll(
'{{nextVersionDetails}}', "{{nextVersionDetails}}",
'- ' + nextCommitObject.recommendedNextVersionDetails.join('\n- '), "- " + nextCommitObject.recommendedNextVersionDetails.join("\n- "),
); );
} else { } else {
changelog = changelog.replaceAll('\n{{nextVersionDetails}}', ''); changelog = changelog.replaceAll("\n{{nextVersionDetails}}", "");
} }
await plugins.smartfs await plugins.smartfs
.file(plugins.path.join(paths.cwd, `changelog.md`)) .file(plugins.path.join(paths.cwd, `changelog.md`))
.encoding('utf8') .encoding("utf8")
.write(changelog); .write(changelog);
ui.printStep(currentStep, totalSteps, '📄 Generating changelog.md', 'done'); ui.printStep(currentStep, totalSteps, "📄 Generating changelog.md", "done");
// Step 3: Staging files // Step 3: Staging files
currentStep++; currentStep++;
ui.printStep(currentStep, totalSteps, '📦 Staging files', 'in-progress'); ui.printStep(currentStep, totalSteps, "📦 Staging files", "in-progress");
await smartshellInstance.exec(`git add -A`); await smartshellInstance.exec(`git add -A`);
ui.printStep(currentStep, totalSteps, '📦 Staging files', 'done'); ui.printStep(currentStep, totalSteps, "📦 Staging files", "done");
// Step 4: Creating commit // Step 4: Creating commit
currentStep++; currentStep++;
ui.printStep(currentStep, totalSteps, '💾 Creating git commit', 'in-progress'); ui.printStep(
currentStep,
totalSteps,
"💾 Creating git commit",
"in-progress",
);
await smartshellInstance.exec(`git commit -m "${commitString}"`); await smartshellInstance.exec(`git commit -m "${commitString}"`);
ui.printStep(currentStep, totalSteps, '💾 Creating git commit', 'done'); ui.printStep(currentStep, totalSteps, "💾 Creating git commit", "done");
// Step 5: Bumping version // Step 5: Bumping version
currentStep++; currentStep++;
const projectType = await helpers.detectProjectType(); const projectType = await helpers.detectProjectType();
const newVersion = await helpers.bumpProjectVersion(projectType, commitVersionType, currentStep, totalSteps); const newVersion = await helpers.bumpProjectVersion(
projectType,
commitVersionType,
currentStep,
totalSteps,
);
// Step 6: Run build (optional) // Step 6: Run build (optional)
if (wantsBuild) { if (wantsBuild) {
currentStep++; currentStep++;
ui.printStep(currentStep, totalSteps, '🔨 Running build', 'in-progress'); ui.printStep(currentStep, totalSteps, "🔨 Running build", "in-progress");
const buildResult = await smartshellInstance.exec('pnpm build'); const buildResult = await smartshellInstance.exec("pnpm build");
if (buildResult.exitCode !== 0) { if (buildResult.exitCode !== 0) {
ui.printStep(currentStep, totalSteps, '🔨 Running build', 'error'); ui.printStep(currentStep, totalSteps, "🔨 Running build", "error");
logger.log('error', 'Build failed. Aborting release.'); logger.log("error", "Build failed. Aborting release.");
process.exit(1); process.exit(1);
} }
ui.printStep(currentStep, totalSteps, '🔨 Running build', 'done'); ui.printStep(currentStep, totalSteps, "🔨 Running build", "done");
// Step 7: Verify no uncommitted changes // Step 7: Verify no uncommitted changes
currentStep++; currentStep++;
ui.printStep(currentStep, totalSteps, '🔍 Verifying clean working tree', 'in-progress'); ui.printStep(
const statusResult = await smartshellInstance.exec('git status --porcelain'); currentStep,
if (statusResult.stdout.trim() !== '') { totalSteps,
ui.printStep(currentStep, totalSteps, '🔍 Verifying clean working tree', 'error'); "🔍 Verifying clean working tree",
logger.log('error', 'Build produced uncommitted changes. This usually means build output is not gitignored.'); "in-progress",
logger.log('error', 'Uncommitted files:'); );
const statusResult = await smartshellInstance.exec(
"git status --porcelain",
);
if (statusResult.stdout.trim() !== "") {
ui.printStep(
currentStep,
totalSteps,
"🔍 Verifying clean working tree",
"error",
);
logger.log(
"error",
"Build produced uncommitted changes. This usually means build output is not gitignored.",
);
logger.log("error", "Uncommitted files:");
console.log(statusResult.stdout); console.log(statusResult.stdout);
logger.log('error', 'Aborting release. Please ensure build artifacts are in .gitignore'); logger.log(
"error",
"Aborting release. Please ensure build artifacts are in .gitignore",
);
process.exit(1); process.exit(1);
} }
ui.printStep(currentStep, totalSteps, '🔍 Verifying clean working tree', 'done'); ui.printStep(
currentStep,
totalSteps,
"🔍 Verifying clean working tree",
"done",
);
} }
// Step: Push to remote (optional) // Step: Push to remote (optional)
const currentBranch = await helpers.detectCurrentBranch(); const currentBranch = await helpers.detectCurrentBranch();
if (willPush) { if (willPush) {
currentStep++; currentStep++;
ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'in-progress'); ui.printStep(
await smartshellInstance.exec(`git push origin ${currentBranch} --follow-tags`); currentStep,
ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'done'); totalSteps,
`🚀 Pushing to origin/${currentBranch}`,
"in-progress",
);
await smartshellInstance.exec(
`git push origin ${currentBranch} --follow-tags`,
);
ui.printStep(
currentStep,
totalSteps,
`🚀 Pushing to origin/${currentBranch}`,
"done",
);
} }
// Step 7: Publish to npm registries (optional) // Step 7: Publish to npm registries (optional)
@@ -296,51 +401,173 @@ export const run = async (argvArg: any) => {
if (willRelease && releaseConfig) { if (willRelease && releaseConfig) {
currentStep++; currentStep++;
const registries = releaseConfig.getRegistries(); const registries = releaseConfig.getRegistries();
ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'in-progress'); ui.printStep(
currentStep,
totalSteps,
`📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
"in-progress",
);
const accessLevel = releaseConfig.getAccessLevel(); const accessLevel = releaseConfig.getAccessLevel();
for (const registry of registries) { for (const registry of registries) {
try { try {
await smartshellInstance.exec(`npm publish --registry=${registry} --access=${accessLevel}`); await smartshellInstance.exec(
`npm publish --registry=${registry} --access=${accessLevel}`,
);
releasedRegistries.push(registry); releasedRegistries.push(registry);
} catch (error) { } catch (error) {
logger.log('error', `Failed to publish to ${registry}: ${error}`); logger.log("error", `Failed to publish to ${registry}: ${error}`);
} }
} }
if (releasedRegistries.length === registries.length) { if (releasedRegistries.length === registries.length) {
ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'done'); ui.printStep(
currentStep,
totalSteps,
`📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
"done",
);
} else { } else {
ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'error'); ui.printStep(
currentStep,
totalSteps,
`📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
"error",
);
} }
} }
console.log(''); // Add spacing before summary console.log(""); // Add spacing before summary
// Get commit SHA for summary // Get commit SHA for summary
const commitShaResult = await smartshellInstance.exec('git rev-parse --short HEAD'); const commitShaResult = await smartshellInstance.exec(
"git rev-parse --short HEAD",
);
const commitSha = commitShaResult.stdout.trim(); const commitSha = commitShaResult.stdout.trim();
// Print final summary // Print final summary
ui.printSummary({ ui.printSummary({
projectType, projectType,
branch: currentBranch, branch: currentBranch,
commitType: answerBucket.getAnswerFor('commitType'), commitType: answerBucket.getAnswerFor("commitType"),
commitScope: answerBucket.getAnswerFor('commitScope'), commitScope: answerBucket.getAnswerFor("commitScope"),
commitMessage: answerBucket.getAnswerFor('commitDescription'), commitMessage: answerBucket.getAnswerFor("commitDescription"),
newVersion: newVersion, newVersion: newVersion,
commitSha: commitSha, commitSha: commitSha,
pushed: willPush, pushed: willPush,
released: releasedRegistries.length > 0, released: releasedRegistries.length > 0,
releasedRegistries: releasedRegistries.length > 0 ? releasedRegistries : undefined, releasedRegistries:
releasedRegistries.length > 0 ? releasedRegistries : undefined,
}); });
}; };
async function handleRecommend(mode: ICliMode): Promise<void> {
const recommendationBuilder = async () => {
const aidoc = new plugins.tsdoc.AiDoc();
await aidoc.start();
try {
return await aidoc.buildNextCommitObject(paths.cwd);
} finally {
await aidoc.stop();
}
};
const recommendation = mode.json
? await runWithSuppressedOutput(recommendationBuilder)
: await recommendationBuilder();
if (mode.json) {
printJson(recommendation);
return;
}
ui.printRecommendation({
recommendedNextVersion: recommendation.recommendedNextVersion,
recommendedNextVersionLevel: recommendation.recommendedNextVersionLevel,
recommendedNextVersionScope: recommendation.recommendedNextVersionScope,
recommendedNextVersionMessage: recommendation.recommendedNextVersionMessage,
});
console.log(
`Suggested commit: ${recommendation.recommendedNextVersionLevel}(${recommendation.recommendedNextVersionScope}): ${recommendation.recommendedNextVersionMessage}`,
);
}
const createCommitStringFromAnswerBucket = ( const createCommitStringFromAnswerBucket = (
answerBucket: plugins.smartinteract.AnswerBucket, answerBucket: plugins.smartinteract.AnswerBucket,
) => { ) => {
const commitType = answerBucket.getAnswerFor('commitType'); const commitType = answerBucket.getAnswerFor("commitType");
const commitScope = answerBucket.getAnswerFor('commitScope'); const commitScope = answerBucket.getAnswerFor("commitScope");
const commitDescription = answerBucket.getAnswerFor('commitDescription'); const commitDescription = answerBucket.getAnswerFor("commitDescription");
return `${commitType}(${commitScope}): ${commitDescription}`; return `${commitType}(${commitScope}): ${commitDescription}`;
}; };
export function showHelp(mode?: ICliMode): void {
if (mode?.json) {
printJson({
command: "commit",
usage: "gitzone commit [recommend] [options]",
description:
"Creates semantic commits or emits a read-only recommendation.",
commands: [
{
name: "recommend",
description:
"Generate a commit recommendation without mutating the repository",
},
],
flags: [
{ flag: "-y, --yes", description: "Auto-accept AI recommendations" },
{ flag: "-p, --push", description: "Push to origin after commit" },
{ flag: "-t, --test", description: "Run tests before the commit flow" },
{
flag: "-b, --build",
description: "Run the build after the commit flow",
},
{
flag: "-r, --release",
description: "Publish to configured registries after push",
},
{
flag: "--format",
description: "Run gitzone format before committing",
},
{
flag: "--json",
description: "Emit JSON for `commit recommend` only",
},
],
examples: [
"gitzone commit recommend --json",
"gitzone commit -y",
"gitzone commit -ypbr",
],
});
return;
}
console.log("");
console.log("Usage: gitzone commit [recommend] [options]");
console.log("");
console.log("Commands:");
console.log(
" recommend Generate a commit recommendation without mutating the repository",
);
console.log("");
console.log("Flags:");
console.log(" -y, --yes Auto-accept AI recommendations");
console.log(" -p, --push Push to origin after commit");
console.log(" -t, --test Run tests before the commit flow");
console.log(" -b, --build Run the build after the commit flow");
console.log(
" -r, --release Publish to configured registries after push",
);
console.log(" --format Run gitzone format before committing");
console.log(" --json Emit JSON for `commit recommend` only");
console.log("");
console.log("Examples:");
console.log(" gitzone commit recommend --json");
console.log(" gitzone commit -y");
console.log(" gitzone commit -ypbr");
console.log("");
}
+490 -182
View File
@@ -1,73 +1,116 @@
// gitzone config - manage release registry configuration // gitzone config - manage release registry configuration
import * as plugins from './mod.plugins.js'; import * as plugins from "./mod.plugins.js";
import { ReleaseConfig } from './classes.releaseconfig.js'; import { ReleaseConfig } from "./classes.releaseconfig.js";
import { CommitConfig } from './classes.commitconfig.js'; import { CommitConfig } from "./classes.commitconfig.js";
import { runFormatter, type ICheckResult } from '../mod_format/index.js'; import { runFormatter, type ICheckResult } from "../mod_format/index.js";
import type { ICliMode } from "../helpers.climode.js";
import { getCliMode, printJson } from "../helpers.climode.js";
import {
getCliConfigValueFromData,
readSmartconfigFile,
setCliConfigValueInData,
unsetCliConfigValueInData,
writeSmartconfigFile,
} from "../helpers.smartconfig.js";
export { ReleaseConfig, CommitConfig }; export { ReleaseConfig, CommitConfig };
const defaultCliMode: ICliMode = {
output: "human",
interactive: true,
json: false,
plain: false,
quiet: false,
yes: false,
help: false,
agent: false,
checkUpdates: true,
isTty: true,
};
/** /**
* Format .smartconfig.json with diff preview * Format .smartconfig.json with diff preview
* Shows diff first, asks for confirmation, then applies * Shows diff first, asks for confirmation, then applies
*/ */
async function formatSmartconfigWithDiff(): Promise<void> { async function formatSmartconfigWithDiff(mode: ICliMode): Promise<void> {
if (!mode.interactive) {
return;
}
// Check for diffs first // Check for diffs first
const checkResult = await runFormatter('smartconfig', { const checkResult = (await runFormatter("smartconfig", {
checkOnly: true, checkOnly: true,
showDiff: true, showDiff: true,
}) as ICheckResult | void; })) as ICheckResult | void;
if (checkResult && checkResult.hasDiff) { if (checkResult && checkResult.hasDiff) {
const shouldApply = await plugins.smartinteract.SmartInteract.getCliConfirmation( const shouldApply =
'Apply formatting changes to .smartconfig.json?', await plugins.smartinteract.SmartInteract.getCliConfirmation(
true "Apply formatting changes to .smartconfig.json?",
); true,
);
if (shouldApply) { if (shouldApply) {
await runFormatter('smartconfig', { silent: true }); await runFormatter("smartconfig", { silent: true });
} }
} }
} }
export const run = async (argvArg: any) => { export const run = async (argvArg: any) => {
const mode = await getCliMode(argvArg);
const command = argvArg._?.[1]; const command = argvArg._?.[1];
const value = argvArg._?.[2]; const value = argvArg._?.[2];
if (mode.help || command === "help") {
showHelp(mode);
return;
}
// If no command provided, show interactive menu // If no command provided, show interactive menu
if (!command) { if (!command) {
if (!mode.interactive) {
showHelp(mode);
return;
}
await handleInteractiveMenu(); await handleInteractiveMenu();
return; return;
} }
switch (command) { switch (command) {
case 'show': case "show":
await handleShow(); await handleShow(mode);
break; break;
case 'add': case "add":
await handleAdd(value); await handleAdd(value, mode);
break; break;
case 'remove': case "remove":
await handleRemove(value); await handleRemove(value, mode);
break; break;
case 'clear': case "clear":
await handleClear(); await handleClear(mode);
break; break;
case 'access': case "access":
case 'accessLevel': case "accessLevel":
await handleAccessLevel(value); await handleAccessLevel(value, mode);
break; break;
case 'commit': case "commit":
await handleCommit(argvArg._?.[2], argvArg._?.[3]); await handleCommit(argvArg._?.[2], argvArg._?.[3], mode);
break; break;
case 'services': case "services":
await handleServices(); await handleServices(mode);
break; break;
case 'help': case "get":
showHelp(); await handleGet(value, mode);
break;
case "set":
await handleSet(value, argvArg._?.[3], mode);
break;
case "unset":
await handleUnset(value, mode);
break; break;
default: default:
plugins.logger.log('error', `Unknown command: ${command}`); plugins.logger.log("error", `Unknown command: ${command}`);
showHelp(); showHelp(mode);
} }
}; };
@@ -75,55 +118,61 @@ export const run = async (argvArg: any) => {
* Interactive menu for config command * Interactive menu for config command
*/ */
async function handleInteractiveMenu(): Promise<void> { async function handleInteractiveMenu(): Promise<void> {
console.log(''); console.log("");
console.log('╭─────────────────────────────────────────────────────────────╮'); console.log(
console.log('│ gitzone config - Project Configuration │'); "╭─────────────────────────────────────────────────────────────╮",
console.log('╰─────────────────────────────────────────────────────────────╯'); );
console.log(''); console.log(
"│ gitzone config - Project Configuration │",
);
console.log(
"╰─────────────────────────────────────────────────────────────╯",
);
console.log("");
const interactInstance = new plugins.smartinteract.SmartInteract(); const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({ const response = await interactInstance.askQuestion({
type: 'list', type: "list",
name: 'action', name: "action",
message: 'What would you like to do?', message: "What would you like to do?",
default: 'show', default: "show",
choices: [ choices: [
{ name: 'Show current configuration', value: 'show' }, { name: "Show current configuration", value: "show" },
{ name: 'Add a registry', value: 'add' }, { name: "Add a registry", value: "add" },
{ name: 'Remove a registry', value: 'remove' }, { name: "Remove a registry", value: "remove" },
{ name: 'Clear all registries', value: 'clear' }, { name: "Clear all registries", value: "clear" },
{ name: 'Set access level (public/private)', value: 'access' }, { name: "Set access level (public/private)", value: "access" },
{ name: 'Configure commit options', value: 'commit' }, { name: "Configure commit options", value: "commit" },
{ name: 'Configure services', value: 'services' }, { name: "Configure services", value: "services" },
{ name: 'Show help', value: 'help' }, { name: "Show help", value: "help" },
], ],
}); });
const action = (response as any).value; const action = (response as any).value;
switch (action) { switch (action) {
case 'show': case "show":
await handleShow(); await handleShow(defaultCliMode);
break; break;
case 'add': case "add":
await handleAdd(); await handleAdd(undefined, defaultCliMode);
break; break;
case 'remove': case "remove":
await handleRemove(); await handleRemove(undefined, defaultCliMode);
break; break;
case 'clear': case "clear":
await handleClear(); await handleClear(defaultCliMode);
break; break;
case 'access': case "access":
await handleAccessLevel(); await handleAccessLevel(undefined, defaultCliMode);
break; break;
case 'commit': case "commit":
await handleCommit(); await handleCommit(undefined, undefined, defaultCliMode);
break; break;
case 'services': case "services":
await handleServices(); await handleServices(defaultCliMode);
break; break;
case 'help': case "help":
showHelp(); showHelp();
break; break;
} }
@@ -132,50 +181,69 @@ async function handleInteractiveMenu(): Promise<void> {
/** /**
* Show current registry configuration * Show current registry configuration
*/ */
async function handleShow(): Promise<void> { async function handleShow(mode: ICliMode): Promise<void> {
if (mode.json) {
const smartconfigData = await readSmartconfigFile();
printJson(getCliConfigValueFromData(smartconfigData, ""));
return;
}
const config = await ReleaseConfig.fromCwd(); const config = await ReleaseConfig.fromCwd();
const registries = config.getRegistries(); const registries = config.getRegistries();
const accessLevel = config.getAccessLevel(); const accessLevel = config.getAccessLevel();
console.log(''); console.log("");
console.log('╭─────────────────────────────────────────────────────────────╮'); console.log(
console.log('│ Release Configuration │'); "╭─────────────────────────────────────────────────────────────╮",
console.log('╰─────────────────────────────────────────────────────────────╯'); );
console.log(''); console.log(
"│ Release Configuration │",
);
console.log(
"╰─────────────────────────────────────────────────────────────╯",
);
console.log("");
// Show access level // Show access level
plugins.logger.log('info', `Access Level: ${accessLevel}`); plugins.logger.log("info", `Access Level: ${accessLevel}`);
console.log(''); console.log("");
if (registries.length === 0) { if (registries.length === 0) {
plugins.logger.log('info', 'No release registries configured.'); plugins.logger.log("info", "No release registries configured.");
console.log(''); console.log("");
console.log(' Run `gitzone config add <registry-url>` to add one.'); console.log(" Run `gitzone config add <registry-url>` to add one.");
console.log(''); console.log("");
} else { } else {
plugins.logger.log('info', `Configured registries (${registries.length}):`); plugins.logger.log("info", `Configured registries (${registries.length}):`);
console.log(''); console.log("");
registries.forEach((url, index) => { registries.forEach((url, index) => {
console.log(` ${index + 1}. ${url}`); console.log(` ${index + 1}. ${url}`);
}); });
console.log(''); console.log("");
} }
} }
/** /**
* Add a registry URL * Add a registry URL
*/ */
async function handleAdd(url?: string): Promise<void> { async function handleAdd(
url: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!url) { if (!url) {
if (!mode.interactive) {
throw new Error("Registry URL is required in non-interactive mode");
}
// Interactive mode // Interactive mode
const interactInstance = new plugins.smartinteract.SmartInteract(); const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({ const response = await interactInstance.askQuestion({
type: 'input', type: "input",
name: 'registryUrl', name: "registryUrl",
message: 'Enter registry URL:', message: "Enter registry URL:",
default: 'https://registry.npmjs.org', default: "https://registry.npmjs.org",
validate: (input: string) => { validate: (input: string) => {
return !!(input && input.trim() !== ''); return !!(input && input.trim() !== "");
}, },
}); });
url = (response as any).value; url = (response as any).value;
@@ -186,32 +254,48 @@ async function handleAdd(url?: string): Promise<void> {
if (added) { if (added) {
await config.save(); await config.save();
plugins.logger.log('success', `Added registry: ${url}`); if (mode.json) {
await formatSmartconfigWithDiff(); printJson({
ok: true,
action: "add",
registry: url,
registries: config.getRegistries(),
});
return;
}
plugins.logger.log("success", `Added registry: ${url}`);
await formatSmartconfigWithDiff(mode);
} else { } else {
plugins.logger.log('warn', `Registry already exists: ${url}`); plugins.logger.log("warn", `Registry already exists: ${url}`);
} }
} }
/** /**
* Remove a registry URL * Remove a registry URL
*/ */
async function handleRemove(url?: string): Promise<void> { async function handleRemove(
url: string | undefined,
mode: ICliMode,
): Promise<void> {
const config = await ReleaseConfig.fromCwd(); const config = await ReleaseConfig.fromCwd();
const registries = config.getRegistries(); const registries = config.getRegistries();
if (registries.length === 0) { if (registries.length === 0) {
plugins.logger.log('warn', 'No registries configured to remove.'); plugins.logger.log("warn", "No registries configured to remove.");
return; return;
} }
if (!url) { if (!url) {
if (!mode.interactive) {
throw new Error("Registry URL is required in non-interactive mode");
}
// Interactive mode - show list to select from // Interactive mode - show list to select from
const interactInstance = new plugins.smartinteract.SmartInteract(); const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({ const response = await interactInstance.askQuestion({
type: 'list', type: "list",
name: 'registryUrl', name: "registryUrl",
message: 'Select registry to remove:', message: "Select registry to remove:",
choices: registries, choices: registries,
default: registries[0], default: registries[0],
}); });
@@ -222,99 +306,135 @@ async function handleRemove(url?: string): Promise<void> {
if (removed) { if (removed) {
await config.save(); await config.save();
plugins.logger.log('success', `Removed registry: ${url}`); if (mode.json) {
await formatSmartconfigWithDiff(); printJson({
ok: true,
action: "remove",
registry: url,
registries: config.getRegistries(),
});
return;
}
plugins.logger.log("success", `Removed registry: ${url}`);
await formatSmartconfigWithDiff(mode);
} else { } else {
plugins.logger.log('warn', `Registry not found: ${url}`); plugins.logger.log("warn", `Registry not found: ${url}`);
} }
} }
/** /**
* Clear all registries * Clear all registries
*/ */
async function handleClear(): Promise<void> { async function handleClear(mode: ICliMode): Promise<void> {
const config = await ReleaseConfig.fromCwd(); const config = await ReleaseConfig.fromCwd();
if (!config.hasRegistries()) { if (!config.hasRegistries()) {
plugins.logger.log('info', 'No registries to clear.'); plugins.logger.log("info", "No registries to clear.");
return; return;
} }
// Confirm before clearing // Confirm before clearing
const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation( const confirmed = mode.interactive
'Clear all configured registries?', ? await plugins.smartinteract.SmartInteract.getCliConfirmation(
false "Clear all configured registries?",
); false,
)
: true;
if (confirmed) { if (confirmed) {
config.clearRegistries(); config.clearRegistries();
await config.save(); await config.save();
plugins.logger.log('success', 'All registries cleared.'); if (mode.json) {
await formatSmartconfigWithDiff(); printJson({ ok: true, action: "clear", registries: [] });
return;
}
plugins.logger.log("success", "All registries cleared.");
await formatSmartconfigWithDiff(mode);
} else { } else {
plugins.logger.log('info', 'Operation cancelled.'); plugins.logger.log("info", "Operation cancelled.");
} }
} }
/** /**
* Set or toggle access level * Set or toggle access level
*/ */
async function handleAccessLevel(level?: string): Promise<void> { async function handleAccessLevel(
level: string | undefined,
mode: ICliMode,
): Promise<void> {
const config = await ReleaseConfig.fromCwd(); const config = await ReleaseConfig.fromCwd();
const currentLevel = config.getAccessLevel(); const currentLevel = config.getAccessLevel();
if (!level) { if (!level) {
if (!mode.interactive) {
throw new Error("Access level is required in non-interactive mode");
}
// Interactive mode - toggle or ask // Interactive mode - toggle or ask
const interactInstance = new plugins.smartinteract.SmartInteract(); const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({ const response = await interactInstance.askQuestion({
type: 'list', type: "list",
name: 'accessLevel', name: "accessLevel",
message: 'Select npm access level for publishing:', message: "Select npm access level for publishing:",
choices: ['public', 'private'], choices: ["public", "private"],
default: currentLevel, default: currentLevel,
}); });
level = (response as any).value; level = (response as any).value;
} }
// Validate the level // Validate the level
if (level !== 'public' && level !== 'private') { if (level !== "public" && level !== "private") {
plugins.logger.log('error', `Invalid access level: ${level}. Must be 'public' or 'private'.`); plugins.logger.log(
"error",
`Invalid access level: ${level}. Must be 'public' or 'private'.`,
);
return; return;
} }
if (level === currentLevel) { if (level === currentLevel) {
plugins.logger.log('info', `Access level is already set to: ${level}`); plugins.logger.log("info", `Access level is already set to: ${level}`);
return; return;
} }
config.setAccessLevel(level as 'public' | 'private'); config.setAccessLevel(level as "public" | "private");
await config.save(); await config.save();
plugins.logger.log('success', `Access level set to: ${level}`); if (mode.json) {
await formatSmartconfigWithDiff(); printJson({ ok: true, action: "access", accessLevel: level });
return;
}
plugins.logger.log("success", `Access level set to: ${level}`);
await formatSmartconfigWithDiff(mode);
} }
/** /**
* Handle commit configuration * Handle commit configuration
*/ */
async function handleCommit(setting?: string, value?: string): Promise<void> { async function handleCommit(
setting: string | undefined,
value: string | undefined,
mode: ICliMode,
): Promise<void> {
const config = await CommitConfig.fromCwd(); const config = await CommitConfig.fromCwd();
// No setting = interactive mode // No setting = interactive mode
if (!setting) { if (!setting) {
if (!mode.interactive) {
throw new Error("Commit setting is required in non-interactive mode");
}
await handleCommitInteractive(config); await handleCommitInteractive(config);
return; return;
} }
// Direct setting // Direct setting
switch (setting) { switch (setting) {
case 'alwaysTest': case "alwaysTest":
await handleCommitSetting(config, 'alwaysTest', value); await handleCommitSetting(config, "alwaysTest", value, mode);
break; break;
case 'alwaysBuild': case "alwaysBuild":
await handleCommitSetting(config, 'alwaysBuild', value); await handleCommitSetting(config, "alwaysBuild", value, mode);
break; break;
default: default:
plugins.logger.log('error', `Unknown commit setting: ${setting}`); plugins.logger.log("error", `Unknown commit setting: ${setting}`);
showCommitHelp(); showCommitHelp();
} }
} }
@@ -323,109 +443,297 @@ async function handleCommit(setting?: string, value?: string): Promise<void> {
* Interactive commit configuration * Interactive commit configuration
*/ */
async function handleCommitInteractive(config: CommitConfig): Promise<void> { async function handleCommitInteractive(config: CommitConfig): Promise<void> {
console.log(''); console.log("");
console.log('╭─────────────────────────────────────────────────────────────╮'); console.log(
console.log('│ Commit Configuration │'); "╭─────────────────────────────────────────────────────────────╮",
console.log('╰─────────────────────────────────────────────────────────────╯'); );
console.log(''); console.log(
"│ Commit Configuration │",
);
console.log(
"╰─────────────────────────────────────────────────────────────╯",
);
console.log("");
const interactInstance = new plugins.smartinteract.SmartInteract(); const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({ const response = await interactInstance.askQuestion({
type: 'checkbox', type: "checkbox",
name: 'commitOptions', name: "commitOptions",
message: 'Select commit options to enable:', message: "Select commit options to enable:",
choices: [ choices: [
{ name: 'Always run tests before commit (-t)', value: 'alwaysTest' }, { name: "Always run tests before commit (-t)", value: "alwaysTest" },
{ name: 'Always build after commit (-b)', value: 'alwaysBuild' }, { name: "Always build after commit (-b)", value: "alwaysBuild" },
], ],
default: [ default: [
...(config.getAlwaysTest() ? ['alwaysTest'] : []), ...(config.getAlwaysTest() ? ["alwaysTest"] : []),
...(config.getAlwaysBuild() ? ['alwaysBuild'] : []), ...(config.getAlwaysBuild() ? ["alwaysBuild"] : []),
], ],
}); });
const selected = (response as any).value || []; const selected = (response as any).value || [];
config.setAlwaysTest(selected.includes('alwaysTest')); config.setAlwaysTest(selected.includes("alwaysTest"));
config.setAlwaysBuild(selected.includes('alwaysBuild')); config.setAlwaysBuild(selected.includes("alwaysBuild"));
await config.save(); await config.save();
plugins.logger.log('success', 'Commit configuration updated'); plugins.logger.log("success", "Commit configuration updated");
await formatSmartconfigWithDiff(); await formatSmartconfigWithDiff(defaultCliMode);
} }
/** /**
* Set a specific commit setting * Set a specific commit setting
*/ */
async function handleCommitSetting(config: CommitConfig, setting: string, value?: string): Promise<void> { async function handleCommitSetting(
config: CommitConfig,
setting: string,
value: string | undefined,
mode: ICliMode,
): Promise<void> {
// Parse boolean value // Parse boolean value
const boolValue = value === 'true' || value === '1' || value === 'on'; const boolValue = value === "true" || value === "1" || value === "on";
if (setting === 'alwaysTest') { if (setting === "alwaysTest") {
config.setAlwaysTest(boolValue); config.setAlwaysTest(boolValue);
} else if (setting === 'alwaysBuild') { } else if (setting === "alwaysBuild") {
config.setAlwaysBuild(boolValue); config.setAlwaysBuild(boolValue);
} }
await config.save(); await config.save();
plugins.logger.log('success', `Set ${setting} to ${boolValue}`); if (mode.json) {
await formatSmartconfigWithDiff(); printJson({ ok: true, action: "commit", setting, value: boolValue });
return;
}
plugins.logger.log("success", `Set ${setting} to ${boolValue}`);
await formatSmartconfigWithDiff(mode);
} }
/** /**
* Show help for commit subcommand * Show help for commit subcommand
*/ */
function showCommitHelp(): void { function showCommitHelp(): void {
console.log(''); console.log("");
console.log('Usage: gitzone config commit [setting] [value]'); console.log("Usage: gitzone config commit [setting] [value]");
console.log(''); console.log("");
console.log('Settings:'); console.log("Settings:");
console.log(' alwaysTest [true|false] Always run tests before commit'); console.log(" alwaysTest [true|false] Always run tests before commit");
console.log(' alwaysBuild [true|false] Always build after commit'); console.log(" alwaysBuild [true|false] Always build after commit");
console.log(''); console.log("");
console.log('Examples:'); console.log("Examples:");
console.log(' gitzone config commit # Interactive mode'); console.log(" gitzone config commit # Interactive mode");
console.log(' gitzone config commit alwaysTest true'); console.log(" gitzone config commit alwaysTest true");
console.log(' gitzone config commit alwaysBuild false'); console.log(" gitzone config commit alwaysBuild false");
console.log(''); console.log("");
} }
/** /**
* Handle services configuration * Handle services configuration
*/ */
async function handleServices(): Promise<void> { async function handleServices(mode: ICliMode): Promise<void> {
if (!mode.interactive) {
throw new Error(
"Use `gitzone services config --json` or `gitzone services set ...` in non-interactive mode",
);
}
// Import and use ServiceManager's configureServices // Import and use ServiceManager's configureServices
const { ServiceManager } = await import('../mod_services/classes.servicemanager.js'); const { ServiceManager } =
await import("../mod_services/classes.servicemanager.js");
const serviceManager = new ServiceManager(); const serviceManager = new ServiceManager();
await serviceManager.init(); await serviceManager.init();
await serviceManager.configureServices(); await serviceManager.configureServices();
} }
async function handleGet(
configPath: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!configPath) {
throw new Error("Configuration path is required");
}
const smartconfigData = await readSmartconfigFile();
const value = getCliConfigValueFromData(smartconfigData, configPath);
if (mode.json) {
printJson({ path: configPath, value, exists: value !== undefined });
return;
}
if (value === undefined) {
plugins.logger.log("warn", `No value set for ${configPath}`);
return;
}
if (typeof value === "string") {
console.log(value);
return;
}
printJson(value);
}
async function handleSet(
configPath: string | undefined,
rawValue: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!configPath) {
throw new Error("Configuration path is required");
}
if (rawValue === undefined) {
throw new Error("Configuration value is required");
}
const smartconfigData = await readSmartconfigFile();
const parsedValue = parseConfigValue(rawValue);
setCliConfigValueInData(smartconfigData, configPath, parsedValue);
await writeSmartconfigFile(smartconfigData);
if (mode.json) {
printJson({
ok: true,
action: "set",
path: configPath,
value: parsedValue,
});
return;
}
plugins.logger.log("success", `Set ${configPath}`);
}
async function handleUnset(
configPath: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!configPath) {
throw new Error("Configuration path is required");
}
const smartconfigData = await readSmartconfigFile();
const removed = unsetCliConfigValueInData(smartconfigData, configPath);
if (!removed) {
if (mode.json) {
printJson({
ok: false,
action: "unset",
path: configPath,
removed: false,
});
return;
}
plugins.logger.log("warn", `No value set for ${configPath}`);
return;
}
await writeSmartconfigFile(smartconfigData);
if (mode.json) {
printJson({ ok: true, action: "unset", path: configPath, removed: true });
return;
}
plugins.logger.log("success", `Unset ${configPath}`);
}
function parseConfigValue(rawValue: string): any {
const trimmedValue = rawValue.trim();
if (trimmedValue === "true") {
return true;
}
if (trimmedValue === "false") {
return false;
}
if (trimmedValue === "null") {
return null;
}
if (/^-?\d+(\.\d+)?$/.test(trimmedValue)) {
return Number(trimmedValue);
}
if (
(trimmedValue.startsWith("{") && trimmedValue.endsWith("}")) ||
(trimmedValue.startsWith("[") && trimmedValue.endsWith("]")) ||
(trimmedValue.startsWith('"') && trimmedValue.endsWith('"'))
) {
return JSON.parse(trimmedValue);
}
return rawValue;
}
/** /**
* Show help for config command * Show help for config command
*/ */
function showHelp(): void { export function showHelp(mode?: ICliMode): void {
console.log(''); if (mode?.json) {
console.log('Usage: gitzone config <command> [options]'); printJson({
console.log(''); command: "config",
console.log('Commands:'); usage: "gitzone config <command> [options]",
console.log(' show Display current release configuration'); commands: [
console.log(' add [url] Add a registry URL'); {
console.log(' remove [url] Remove a registry URL'); name: "show",
console.log(' clear Clear all registries'); description: "Display current @git.zone/cli configuration",
console.log(' access [public|private] Set npm access level for publishing'); },
console.log(' commit [setting] [value] Configure commit options'); { name: "get <path>", description: "Read a single config value" },
console.log(' services Configure which services are enabled'); { name: "set <path> <value>", description: "Write a config value" },
console.log(''); { name: "unset <path>", description: "Delete a config value" },
console.log('Examples:'); { name: "add [url]", description: "Add a release registry" },
console.log(' gitzone config show'); { name: "remove [url]", description: "Remove a release registry" },
console.log(' gitzone config add https://registry.npmjs.org'); { name: "clear", description: "Clear all release registries" },
console.log(' gitzone config add https://verdaccio.example.com'); {
console.log(' gitzone config remove https://registry.npmjs.org'); name: "access [public|private]",
console.log(' gitzone config clear'); description: "Set npm publish access level",
console.log(' gitzone config access public'); },
console.log(' gitzone config access private'); {
console.log(' gitzone config commit # Interactive'); name: "commit <setting> <value>",
console.log(' gitzone config commit alwaysTest true'); description: "Set commit defaults",
console.log(' gitzone config services # Interactive'); },
console.log(''); ],
examples: [
"gitzone config show --json",
"gitzone config get release.accessLevel",
"gitzone config set cli.interactive false",
"gitzone config set cli.output json",
],
});
return;
}
console.log("");
console.log("Usage: gitzone config <command> [options]");
console.log("");
console.log("Commands:");
console.log(
" show Display current @git.zone/cli configuration",
);
console.log(" get <path> Read a single config value");
console.log(" set <path> <value> Write a config value");
console.log(" unset <path> Delete a config value");
console.log(" add [url] Add a registry URL");
console.log(" remove [url] Remove a registry URL");
console.log(" clear Clear all registries");
console.log(
" access [public|private] Set npm access level for publishing",
);
console.log(" commit [setting] [value] Configure commit options");
console.log(
" services Configure which services are enabled",
);
console.log("");
console.log("Examples:");
console.log(" gitzone config show");
console.log(" gitzone config show --json");
console.log(" gitzone config get release.accessLevel");
console.log(" gitzone config set cli.interactive false");
console.log(" gitzone config set cli.output json");
console.log(" gitzone config unset cli.output");
console.log(" gitzone config add https://registry.npmjs.org");
console.log(" gitzone config add https://verdaccio.example.com");
console.log(" gitzone config remove https://registry.npmjs.org");
console.log(" gitzone config clear");
console.log(" gitzone config access public");
console.log(" gitzone config access private");
console.log(" gitzone config commit # Interactive");
console.log(" gitzone config commit alwaysTest true");
console.log(" gitzone config services # Interactive");
console.log("");
} }
+20 -3
View File
@@ -1,14 +1,31 @@
import * as plugins from './mod.plugins.js'; import * as plugins from "./mod.plugins.js";
import { FormatStats } from './classes.formatstats.js'; import { FormatStats } from "./classes.formatstats.js";
interface IFormatContextOptions {
interactive?: boolean;
jsonOutput?: boolean;
}
export class FormatContext { export class FormatContext {
private formatStats: FormatStats; private formatStats: FormatStats;
private interactive: boolean;
private jsonOutput: boolean;
constructor() { constructor(options: IFormatContextOptions = {}) {
this.formatStats = new FormatStats(); this.formatStats = new FormatStats();
this.interactive = options.interactive ?? true;
this.jsonOutput = options.jsonOutput ?? false;
} }
getFormatStats(): FormatStats { getFormatStats(): FormatStats {
return this.formatStats; return this.formatStats;
} }
isInteractive(): boolean {
return this.interactive;
}
isJsonOutput(): boolean {
return this.jsonOutput;
}
} }
@@ -1,7 +1,7 @@
import { BaseFormatter } from '../classes.baseformatter.js'; import { BaseFormatter } from "../classes.baseformatter.js";
import type { IPlannedChange } from '../interfaces.format.js'; import type { IPlannedChange } from "../interfaces.format.js";
import * as plugins from '../mod.plugins.js'; import * as plugins from "../mod.plugins.js";
import { logger, logVerbose } from '../../gitzone.logging.js'; import { logger, logVerbose } from "../../gitzone.logging.js";
/** /**
* Migrates .smartconfig.json from old namespace keys to new package-scoped keys * Migrates .smartconfig.json from old namespace keys to new package-scoped keys
@@ -9,11 +9,11 @@ import { logger, logVerbose } from '../../gitzone.logging.js';
const migrateNamespaceKeys = (smartconfigJson: any): boolean => { const migrateNamespaceKeys = (smartconfigJson: any): boolean => {
let migrated = false; let migrated = false;
const migrations = [ const migrations = [
{ oldKey: 'gitzone', newKey: '@git.zone/cli' }, { oldKey: "gitzone", newKey: "@git.zone/cli" },
{ oldKey: 'tsdoc', newKey: '@git.zone/tsdoc' }, { oldKey: "tsdoc", newKey: "@git.zone/tsdoc" },
{ oldKey: 'npmdocker', newKey: '@git.zone/tsdocker' }, { oldKey: "npmdocker", newKey: "@git.zone/tsdocker" },
{ oldKey: 'npmci', newKey: '@ship.zone/szci' }, { oldKey: "npmci", newKey: "@ship.zone/szci" },
{ oldKey: 'szci', newKey: '@ship.zone/szci' }, { oldKey: "szci", newKey: "@ship.zone/szci" },
]; ];
for (const { oldKey, newKey } of migrations) { for (const { oldKey, newKey } of migrations) {
if (smartconfigJson[oldKey]) { if (smartconfigJson[oldKey]) {
@@ -36,36 +36,37 @@ const migrateNamespaceKeys = (smartconfigJson: any): boolean => {
* Migrates npmAccessLevel from @ship.zone/szci to @git.zone/cli.release.accessLevel * Migrates npmAccessLevel from @ship.zone/szci to @git.zone/cli.release.accessLevel
*/ */
const migrateAccessLevel = (smartconfigJson: any): boolean => { const migrateAccessLevel = (smartconfigJson: any): boolean => {
const szciConfig = smartconfigJson['@ship.zone/szci']; const szciConfig = smartconfigJson["@ship.zone/szci"];
if (!szciConfig?.npmAccessLevel) { if (!szciConfig?.npmAccessLevel) {
return false; return false;
} }
const gitzoneConfig = smartconfigJson['@git.zone/cli'] || {}; const gitzoneConfig = smartconfigJson["@git.zone/cli"] || {};
if (gitzoneConfig?.release?.accessLevel) { if (gitzoneConfig?.release?.accessLevel) {
delete szciConfig.npmAccessLevel; delete szciConfig.npmAccessLevel;
return true; return true;
} }
if (!smartconfigJson['@git.zone/cli']) { if (!smartconfigJson["@git.zone/cli"]) {
smartconfigJson['@git.zone/cli'] = {}; smartconfigJson["@git.zone/cli"] = {};
} }
if (!smartconfigJson['@git.zone/cli'].release) { if (!smartconfigJson["@git.zone/cli"].release) {
smartconfigJson['@git.zone/cli'].release = {}; smartconfigJson["@git.zone/cli"].release = {};
} }
smartconfigJson['@git.zone/cli'].release.accessLevel = szciConfig.npmAccessLevel; smartconfigJson["@git.zone/cli"].release.accessLevel =
szciConfig.npmAccessLevel;
delete szciConfig.npmAccessLevel; delete szciConfig.npmAccessLevel;
return true; return true;
}; };
const CONFIG_FILE = '.smartconfig.json'; const CONFIG_FILE = ".smartconfig.json";
export class SmartconfigFormatter extends BaseFormatter { export class SmartconfigFormatter extends BaseFormatter {
get name(): string { get name(): string {
return 'smartconfig'; return "smartconfig";
} }
async analyze(): Promise<IPlannedChange[]> { async analyze(): Promise<IPlannedChange[]> {
@@ -76,13 +77,13 @@ export class SmartconfigFormatter extends BaseFormatter {
// This formatter only operates on .smartconfig.json. // This formatter only operates on .smartconfig.json.
const exists = await plugins.smartfs.file(CONFIG_FILE).exists(); const exists = await plugins.smartfs.file(CONFIG_FILE).exists();
if (!exists) { if (!exists) {
logVerbose('.smartconfig.json does not exist, skipping'); logVerbose(".smartconfig.json does not exist, skipping");
return changes; return changes;
} }
const currentContent = (await plugins.smartfs const currentContent = (await plugins.smartfs
.file(CONFIG_FILE) .file(CONFIG_FILE)
.encoding('utf8') .encoding("utf8")
.read()) as string; .read()) as string;
const smartconfigJson = JSON.parse(currentContent); const smartconfigJson = JSON.parse(currentContent);
@@ -92,21 +93,21 @@ export class SmartconfigFormatter extends BaseFormatter {
migrateAccessLevel(smartconfigJson); migrateAccessLevel(smartconfigJson);
// Ensure namespaces exist // Ensure namespaces exist
if (!smartconfigJson['@git.zone/cli']) { if (!smartconfigJson["@git.zone/cli"]) {
smartconfigJson['@git.zone/cli'] = {}; smartconfigJson["@git.zone/cli"] = {};
} }
if (!smartconfigJson['@ship.zone/szci']) { if (!smartconfigJson["@ship.zone/szci"]) {
smartconfigJson['@ship.zone/szci'] = {}; smartconfigJson["@ship.zone/szci"] = {};
} }
const newContent = JSON.stringify(smartconfigJson, null, 2); const newContent = JSON.stringify(smartconfigJson, null, 2);
if (newContent !== currentContent) { if (newContent !== currentContent) {
changes.push({ changes.push({
type: 'modify', type: "modify",
path: CONFIG_FILE, path: CONFIG_FILE,
module: this.name, module: this.name,
description: 'Migrate and format .smartconfig.json', description: "Migrate and format .smartconfig.json",
content: newContent, content: newContent,
}); });
} }
@@ -115,26 +116,41 @@ export class SmartconfigFormatter extends BaseFormatter {
} }
async applyChange(change: IPlannedChange): Promise<void> { async applyChange(change: IPlannedChange): Promise<void> {
if (change.type !== 'modify' || !change.content) return; if (change.type !== "modify" || !change.content) return;
const smartconfigJson = JSON.parse(change.content); const smartconfigJson = JSON.parse(change.content);
// Check for missing required module information // Check for missing required module information
const expectedRepoInformation: string[] = [ const expectedRepoInformation: string[] = [
'projectType', "projectType",
'module.githost', "module.githost",
'module.gitscope', "module.gitscope",
'module.gitrepo', "module.gitrepo",
'module.description', "module.description",
'module.npmPackagename', "module.npmPackagename",
'module.license', "module.license",
]; ];
const interactInstance = new plugins.smartinteract.SmartInteract(); const interactInstance = new plugins.smartinteract.SmartInteract();
const missingRepoInformation = expectedRepoInformation.filter(
(expectedRepoInformationItem) => {
return !plugins.smartobject.smartGet(
smartconfigJson["@git.zone/cli"],
expectedRepoInformationItem,
);
},
);
if (missingRepoInformation.length > 0 && !this.context.isInteractive()) {
throw new Error(
`Missing required .smartconfig.json fields: ${missingRepoInformation.join(", ")}`,
);
}
for (const expectedRepoInformationItem of expectedRepoInformation) { for (const expectedRepoInformationItem of expectedRepoInformation) {
if ( if (
!plugins.smartobject.smartGet( !plugins.smartobject.smartGet(
smartconfigJson['@git.zone/cli'], smartconfigJson["@git.zone/cli"],
expectedRepoInformationItem, expectedRepoInformationItem,
) )
) { ) {
@@ -142,8 +158,8 @@ export class SmartconfigFormatter extends BaseFormatter {
{ {
message: `What is the value of ${expectedRepoInformationItem}`, message: `What is the value of ${expectedRepoInformationItem}`,
name: expectedRepoInformationItem, name: expectedRepoInformationItem,
type: 'input', type: "input",
default: 'undefined variable', default: "undefined variable",
}, },
]); ]);
} }
@@ -156,7 +172,7 @@ export class SmartconfigFormatter extends BaseFormatter {
); );
if (cliProvidedValue) { if (cliProvidedValue) {
plugins.smartobject.smartAdd( plugins.smartobject.smartAdd(
smartconfigJson['@git.zone/cli'], smartconfigJson["@git.zone/cli"],
expectedRepoInformationItem, expectedRepoInformationItem,
cliProvidedValue, cliProvidedValue,
); );
@@ -165,6 +181,6 @@ export class SmartconfigFormatter extends BaseFormatter {
const finalContent = JSON.stringify(smartconfigJson, null, 2); const finalContent = JSON.stringify(smartconfigJson, null, 2);
await this.modifyFile(change.path, finalContent); await this.modifyFile(change.path, finalContent);
logger.log('info', 'Updated .smartconfig.json'); logger.log("info", "Updated .smartconfig.json");
} }
} }
+277 -88
View File
@@ -1,44 +1,60 @@
import * as plugins from './mod.plugins.js'; import * as plugins from "./mod.plugins.js";
import { Project } from '../classes.project.js'; import { Project } from "../classes.project.js";
import { FormatContext } from './classes.formatcontext.js'; import { FormatContext } from "./classes.formatcontext.js";
import { FormatPlanner } from './classes.formatplanner.js'; import { FormatPlanner } from "./classes.formatplanner.js";
import { BaseFormatter } from './classes.baseformatter.js'; import { BaseFormatter } from "./classes.baseformatter.js";
import { logger, setVerboseMode } from '../gitzone.logging.js'; import { logger, setVerboseMode } from "../gitzone.logging.js";
import type { ICliMode } from "../helpers.climode.js";
import {
getCliMode,
printJson,
runWithSuppressedOutput,
} from "../helpers.climode.js";
import { getCliConfigValue } from "../helpers.smartconfig.js";
import { CleanupFormatter } from './formatters/cleanup.formatter.js'; import { CleanupFormatter } from "./formatters/cleanup.formatter.js";
import { SmartconfigFormatter } from './formatters/smartconfig.formatter.js'; import { SmartconfigFormatter } from "./formatters/smartconfig.formatter.js";
import { LicenseFormatter } from './formatters/license.formatter.js'; import { LicenseFormatter } from "./formatters/license.formatter.js";
import { PackageJsonFormatter } from './formatters/packagejson.formatter.js'; import { PackageJsonFormatter } from "./formatters/packagejson.formatter.js";
import { TemplatesFormatter } from './formatters/templates.formatter.js'; import { TemplatesFormatter } from "./formatters/templates.formatter.js";
import { GitignoreFormatter } from './formatters/gitignore.formatter.js'; import { GitignoreFormatter } from "./formatters/gitignore.formatter.js";
import { TsconfigFormatter } from './formatters/tsconfig.formatter.js'; import { TsconfigFormatter } from "./formatters/tsconfig.formatter.js";
import { PrettierFormatter } from './formatters/prettier.formatter.js'; import { PrettierFormatter } from "./formatters/prettier.formatter.js";
import { ReadmeFormatter } from './formatters/readme.formatter.js'; import { ReadmeFormatter } from "./formatters/readme.formatter.js";
import { CopyFormatter } from './formatters/copy.formatter.js'; import { CopyFormatter } from "./formatters/copy.formatter.js";
/** /**
* Rename npmextra.json or smartconfig.json to .smartconfig.json * Rename npmextra.json or smartconfig.json to .smartconfig.json
* before any formatter tries to read config. * before any formatter tries to read config.
*/ */
async function migrateConfigFile(): Promise<void> { async function migrateConfigFile(allowWrite: boolean): Promise<void> {
const target = '.smartconfig.json'; const target = ".smartconfig.json";
const targetExists = await plugins.smartfs.file(target).exists(); const targetExists = await plugins.smartfs.file(target).exists();
if (targetExists) return; if (targetExists) return;
for (const oldName of ['smartconfig.json', 'npmextra.json']) { for (const oldName of ["smartconfig.json", "npmextra.json"]) {
const exists = await plugins.smartfs.file(oldName).exists(); const exists = await plugins.smartfs.file(oldName).exists();
if (exists) { if (exists) {
const content = await plugins.smartfs.file(oldName).encoding('utf8').read() as string; if (!allowWrite) {
await plugins.smartfs.file(`./${target}`).encoding('utf8').write(content); return;
}
const content = (await plugins.smartfs
.file(oldName)
.encoding("utf8")
.read()) as string;
await plugins.smartfs.file(`./${target}`).encoding("utf8").write(content);
await plugins.smartfs.file(oldName).delete(); await plugins.smartfs.file(oldName).delete();
logger.log('info', `Migrated ${oldName} to ${target}`); logger.log("info", `Migrated ${oldName} to ${target}`);
return; return;
} }
} }
} }
// Shared formatter class map used by both run() and runFormatter() // Shared formatter class map used by both run() and runFormatter()
const formatterMap: Record<string, new (ctx: FormatContext, proj: Project) => BaseFormatter> = { const formatterMap: Record<
string,
new (ctx: FormatContext, proj: Project) => BaseFormatter
> = {
cleanup: CleanupFormatter, cleanup: CleanupFormatter,
smartconfig: SmartconfigFormatter, smartconfig: SmartconfigFormatter,
license: LicenseFormatter, license: LicenseFormatter,
@@ -52,7 +68,104 @@ const formatterMap: Record<string, new (ctx: FormatContext, proj: Project) => Ba
}; };
// Formatters that don't require projectType to be set // Formatters that don't require projectType to be set
const formattersNotRequiringProjectType = ['smartconfig', 'prettier', 'cleanup', 'packagejson']; const formattersNotRequiringProjectType = [
"smartconfig",
"prettier",
"cleanup",
"packagejson",
];
const getFormatConfig = async () => {
const rawFormatConfig = await getCliConfigValue<Record<string, any>>(
"format",
{},
);
return {
interactive: true,
showDiffs: false,
autoApprove: false,
showStats: true,
modules: {
skip: [],
only: [],
...(rawFormatConfig.modules || {}),
},
...rawFormatConfig,
};
};
const createActiveFormatters = async (options: {
interactive: boolean;
jsonOutput: boolean;
}) => {
const project = await Project.fromCwd({ requireProjectType: false });
const context = new FormatContext(options);
const planner = new FormatPlanner();
const formatConfig = await getFormatConfig();
const formatters = Object.entries(formatterMap).map(
([, FormatterClass]) => new FormatterClass(context, project),
);
const activeFormatters = formatters.filter((formatter) => {
if (formatConfig.modules.only.length > 0) {
return formatConfig.modules.only.includes(formatter.name);
}
if (formatConfig.modules.skip.includes(formatter.name)) {
return false;
}
return true;
});
return {
context,
planner,
formatConfig,
activeFormatters,
};
};
const buildFormatPlan = async (options: {
fromPlan?: string;
interactive: boolean;
jsonOutput: boolean;
}) => {
const { context, planner, formatConfig, activeFormatters } =
await createActiveFormatters({
interactive: options.interactive,
jsonOutput: options.jsonOutput,
});
const plan = options.fromPlan
? JSON.parse(
(await plugins.smartfs
.file(options.fromPlan)
.encoding("utf8")
.read()) as string,
)
: await planner.planFormat(activeFormatters);
return {
context,
planner,
formatConfig,
activeFormatters,
plan,
};
};
const serializePlan = (plan: any) => {
return {
summary: plan.summary,
warnings: plan.warnings,
changes: plan.changes.map((change: any) => ({
type: change.type,
path: change.path,
module: change.module,
description: change.description,
})),
};
};
export let run = async ( export let run = async (
options: { options: {
@@ -66,62 +179,61 @@ export let run = async (
interactive?: boolean; interactive?: boolean;
verbose?: boolean; verbose?: boolean;
diff?: boolean; diff?: boolean;
[key: string]: any;
} = {}, } = {},
): Promise<any> => { ): Promise<any> => {
const mode = await getCliMode(options as any);
const subcommand = (options as any)?._?.[1];
if (mode.help || subcommand === "help") {
showHelp(mode);
return;
}
if (options.verbose) { if (options.verbose) {
setVerboseMode(true); setVerboseMode(true);
} }
const shouldWrite = options.write ?? (options.dryRun === false); const shouldWrite = options.write ?? options.dryRun === false;
const treatAsPlan = subcommand === "plan";
if (mode.json && shouldWrite) {
printJson({
ok: false,
error:
"JSON output is only supported for read-only format planning. Use `gitzone format plan --json` or omit `--json` when applying changes.",
});
return;
}
// Migrate config file before anything reads it // Migrate config file before anything reads it
await migrateConfigFile(); await migrateConfigFile(shouldWrite);
const project = await Project.fromCwd({ requireProjectType: false }); const formatConfig = await getFormatConfig();
const context = new FormatContext(); const interactive =
const planner = new FormatPlanner(); options.interactive ?? (mode.interactive && formatConfig.interactive);
const smartconfigInstance = new plugins.smartconfig.Smartconfig();
const formatConfig = smartconfigInstance.dataFor<any>('@git.zone/cli.format', {
interactive: true,
showDiffs: false,
autoApprove: false,
modules: {
skip: [],
only: [],
},
});
const interactive = options.interactive ?? formatConfig.interactive;
const autoApprove = options.yes ?? formatConfig.autoApprove; const autoApprove = options.yes ?? formatConfig.autoApprove;
try { try {
// Initialize formatters in execution order const planBuilder = async () => {
const formatters = Object.entries(formatterMap).map( return await buildFormatPlan({
([, FormatterClass]) => new FormatterClass(context, project), fromPlan: options.fromPlan,
); interactive,
jsonOutput: mode.json,
});
};
// Filter formatters based on configuration if (!mode.json) {
const activeFormatters = formatters.filter((formatter) => { logger.log("info", "Analyzing project for format operations...");
if (formatConfig.modules.only.length > 0) { }
return formatConfig.modules.only.includes(formatter.name); const { context, planner, activeFormatters, plan } = mode.json
} ? await runWithSuppressedOutput(planBuilder)
if (formatConfig.modules.skip.includes(formatter.name)) { : await planBuilder();
return false;
}
return true;
});
// Plan phase if (mode.json) {
logger.log('info', 'Analyzing project for format operations...'); printJson(serializePlan(plan));
let plan = options.fromPlan return;
? JSON.parse( }
(await plugins.smartfs
.file(options.fromPlan)
.encoding('utf8')
.read()) as string,
)
: await planner.planFormat(activeFormatters);
// Display plan // Display plan
await planner.displayPlan(plan, options.detailed); await planner.displayPlan(plan, options.detailed);
@@ -130,34 +242,35 @@ export let run = async (
if (options.savePlan) { if (options.savePlan) {
await plugins.smartfs await plugins.smartfs
.file(options.savePlan) .file(options.savePlan)
.encoding('utf8') .encoding("utf8")
.write(JSON.stringify(plan, null, 2)); .write(JSON.stringify(plan, null, 2));
logger.log('info', `Plan saved to ${options.savePlan}`); logger.log("info", `Plan saved to ${options.savePlan}`);
} }
if (options.planOnly) { if (options.planOnly || treatAsPlan) {
return; return;
} }
// Show diffs if explicitly requested or before interactive write confirmation // Show diffs if explicitly requested or before interactive write confirmation
const showDiffs = options.diff || (shouldWrite && interactive && !autoApprove); const showDiffs =
options.diff || (shouldWrite && interactive && !autoApprove);
if (showDiffs) { if (showDiffs) {
logger.log('info', 'Showing file diffs:'); logger.log("info", "Showing file diffs:");
console.log(''); console.log("");
for (const formatter of activeFormatters) { for (const formatter of activeFormatters) {
const checkResult = await formatter.check(); const checkResult = await formatter.check();
if (checkResult.hasDiff) { if (checkResult.hasDiff) {
logger.log('info', `[${formatter.name}]`); logger.log("info", `[${formatter.name}]`);
formatter.displayAllDiffs(checkResult); formatter.displayAllDiffs(checkResult);
console.log(''); console.log("");
} }
} }
} }
// Dry-run mode (default behavior) // Dry-run mode (default behavior)
if (!shouldWrite) { if (!shouldWrite) {
logger.log('info', 'Dry-run mode - use --write (-w) to apply changes'); logger.log("info", "Dry-run mode - use --write (-w) to apply changes");
return; return;
} }
@@ -165,25 +278,25 @@ export let run = async (
if (interactive && !autoApprove) { if (interactive && !autoApprove) {
const interactInstance = new plugins.smartinteract.SmartInteract(); const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({ const response = await interactInstance.askQuestion({
type: 'confirm', type: "confirm",
name: 'proceed', name: "proceed",
message: 'Proceed with formatting?', message: "Proceed with formatting?",
default: true, default: true,
}); });
if (!(response as any).value) { if (!(response as any).value) {
logger.log('info', 'Format operation cancelled by user'); logger.log("info", "Format operation cancelled by user");
return; return;
} }
} }
// Execute phase // Execute phase
logger.log('info', 'Executing format operations...'); logger.log("info", "Executing format operations...");
await planner.executePlan(plan, activeFormatters, context); await planner.executePlan(plan, activeFormatters, context);
context.getFormatStats().finish(); context.getFormatStats().finish();
const showStats = smartconfigInstance.dataFor('gitzone.format.showStats', true); const showStats = formatConfig.showStats ?? true;
if (showStats) { if (showStats) {
context.getFormatStats().displayStats(); context.getFormatStats().displayStats();
} }
@@ -193,14 +306,15 @@ export let run = async (
await context.getFormatStats().saveReport(statsPath); await context.getFormatStats().saveReport(statsPath);
} }
logger.log('success', 'Format operations completed successfully!'); logger.log("success", "Format operations completed successfully!");
} catch (error) { } catch (error) {
logger.log('error', `Format operation failed: ${error.message}`); const errorMessage = error instanceof Error ? error.message : String(error);
logger.log("error", `Format operation failed: ${errorMessage}`);
throw error; throw error;
} }
}; };
import type { ICheckResult } from './interfaces.format.js'; import type { ICheckResult } from "./interfaces.format.js";
export type { ICheckResult }; export type { ICheckResult };
/** /**
@@ -212,11 +326,12 @@ export const runFormatter = async (
silent?: boolean; silent?: boolean;
checkOnly?: boolean; checkOnly?: boolean;
showDiff?: boolean; showDiff?: boolean;
} = {} } = {},
): Promise<ICheckResult | void> => { ): Promise<ICheckResult | void> => {
const requireProjectType = !formattersNotRequiringProjectType.includes(formatterName); const requireProjectType =
!formattersNotRequiringProjectType.includes(formatterName);
const project = await Project.fromCwd({ requireProjectType }); const project = await Project.fromCwd({ requireProjectType });
const context = new FormatContext(); const context = new FormatContext({ interactive: true, jsonOutput: false });
const FormatterClass = formatterMap[formatterName]; const FormatterClass = formatterMap[formatterName];
if (!FormatterClass) { if (!FormatterClass) {
@@ -240,6 +355,80 @@ export const runFormatter = async (
} }
if (!options.silent) { if (!options.silent) {
logger.log('success', `Formatter '${formatterName}' completed`); logger.log("success", `Formatter '${formatterName}' completed`);
} }
}; };
export function showHelp(mode?: ICliMode): void {
if (mode?.json) {
printJson({
command: "format",
usage: "gitzone format [plan] [options]",
description:
"Plans formatting changes by default and applies them only with --write.",
flags: [
{ flag: "--write, -w", description: "Apply planned changes" },
{
flag: "--yes",
description: "Skip the interactive confirmation before writing",
},
{
flag: "--plan-only",
description: "Show the plan without applying changes",
},
{
flag: "--save-plan <file>",
description: "Write the format plan to a file",
},
{
flag: "--from-plan <file>",
description: "Load a previously saved plan",
},
{
flag: "--detailed",
description: "Show detailed diffs and save stats",
},
{ flag: "--verbose", description: "Enable verbose logging" },
{
flag: "--diff",
description: "Show per-file diffs before applying changes",
},
{ flag: "--json", description: "Emit a read-only format plan as JSON" },
],
examples: [
"gitzone format",
"gitzone format plan --json",
"gitzone format --write --yes",
],
});
return;
}
console.log("");
console.log("Usage: gitzone format [plan] [options]");
console.log("");
console.log(
"Plans formatting changes by default and applies them only with --write.",
);
console.log("");
console.log("Flags:");
console.log(" --write, -w Apply planned changes");
console.log(
" --yes Skip the interactive confirmation before writing",
);
console.log(" --plan-only Show the plan without applying changes");
console.log(" --save-plan <file> Write the format plan to a file");
console.log(" --from-plan <file> Load a previously saved plan");
console.log(" --detailed Show detailed diffs and save stats");
console.log(" --verbose Enable verbose logging");
console.log(
" --diff Show per-file diffs before applying changes",
);
console.log(" --json Emit a read-only format plan as JSON");
console.log("");
console.log("Examples:");
console.log(" gitzone format");
console.log(" gitzone format plan --json");
console.log(" gitzone format --write --yes");
console.log("");
}
+550 -183
View File
@@ -1,12 +1,26 @@
import * as plugins from './mod.plugins.js'; import * as plugins from "./mod.plugins.js";
import * as helpers from './helpers.js'; import * as helpers from "./helpers.js";
import { ServiceManager } from './classes.servicemanager.js'; import { ServiceManager } from "./classes.servicemanager.js";
import { GlobalRegistry } from './classes.globalregistry.js'; import { GlobalRegistry } from "./classes.globalregistry.js";
import { logger } from '../gitzone.logging.js'; import { logger } from "../gitzone.logging.js";
import type { ICliMode } from "../helpers.climode.js";
import { getCliMode, printJson } from "../helpers.climode.js";
import {
getCliConfigValueFromData,
readSmartconfigFile,
setCliConfigValueInData,
writeSmartconfigFile,
} from "../helpers.smartconfig.js";
export const run = async (argvArg: any) => { export const run = async (argvArg: any) => {
const mode = await getCliMode(argvArg);
const isGlobal = argvArg.g || argvArg.global; const isGlobal = argvArg.g || argvArg.global;
const command = argvArg._[1] || 'help'; const command = argvArg._[1] || "help";
if (mode.help || command === "help") {
showHelp(mode);
return;
}
// Handle global commands first // Handle global commands first
if (isGlobal) { if (isGlobal) {
@@ -14,264 +28,597 @@ export const run = async (argvArg: any) => {
return; return;
} }
// Local project commands const service = argvArg._[2] || "all";
const serviceManager = new ServiceManager();
await serviceManager.init();
const service = argvArg._[2] || 'all';
switch (command) { switch (command) {
case 'start': case "config":
await handleStart(serviceManager, service); if (service === "services" || argvArg._[2] === "services") {
break; const serviceManager = new ServiceManager();
await serviceManager.init();
case 'stop':
await handleStop(serviceManager, service);
break;
case 'restart':
await handleRestart(serviceManager, service);
break;
case 'status':
await serviceManager.showStatus();
break;
case 'config':
if (service === 'services' || argvArg._[2] === 'services') {
await handleConfigureServices(serviceManager); await handleConfigureServices(serviceManager);
} else { } else {
await serviceManager.showConfig(); await handleShowConfig(mode);
} }
break; break;
case 'compass': case "set":
await serviceManager.showCompassConnection(); await handleSetServices(argvArg._[2], mode);
break; break;
case 'logs': case "enable":
const lines = parseInt(argvArg._[3]) || 20; await handleEnableServices(argvArg._.slice(2), mode);
await serviceManager.showLogs(service, lines);
break; break;
case 'remove': case "disable":
await handleRemove(serviceManager); await handleDisableServices(argvArg._.slice(2), mode);
break; break;
case 'clean': case "start":
await handleClean(serviceManager); case "stop":
case "restart":
case "status":
case "compass":
case "logs":
case "remove":
case "clean":
case "reconfigure": {
const serviceManager = new ServiceManager();
await serviceManager.init();
switch (command) {
case "start":
await handleStart(serviceManager, service);
break;
case "stop":
await handleStop(serviceManager, service);
break;
case "restart":
await handleRestart(serviceManager, service);
break;
case "status":
await serviceManager.showStatus();
break;
case "compass":
await serviceManager.showCompassConnection();
break;
case "logs": {
const lines = parseInt(argvArg._[3]) || 20;
await serviceManager.showLogs(service, lines);
break;
}
case "remove":
await handleRemove(serviceManager);
break;
case "clean":
await handleClean(serviceManager);
break;
case "reconfigure":
await serviceManager.reconfigure();
break;
}
break; break;
}
case 'reconfigure':
await serviceManager.reconfigure();
break;
case 'help':
default: default:
showHelp(); showHelp(mode);
break; break;
} }
}; };
const allowedServices = ["mongodb", "minio", "elasticsearch"];
const normalizeServiceName = (service: string): string => {
switch (service) {
case "mongo":
case "mongodb":
return "mongodb";
case "minio":
case "s3":
return "minio";
case "elastic":
case "elasticsearch":
case "es":
return "elasticsearch";
default:
return service;
}
};
async function readServicesConfig(): Promise<{
enabledServices: string[];
environment: Record<string, any> | null;
}> {
const smartconfigData = await readSmartconfigFile();
const enabledServices = getCliConfigValueFromData(
smartconfigData,
"services",
);
let environment: Record<string, any> | null = null;
const envPath = plugins.path.join(process.cwd(), ".nogit", "env.json");
if (await plugins.smartfs.file(envPath).exists()) {
const envContent = (await plugins.smartfs
.file(envPath)
.encoding("utf8")
.read()) as string;
environment = JSON.parse(envContent);
}
return {
enabledServices: Array.isArray(enabledServices) ? enabledServices : [],
environment,
};
}
async function updateEnabledServices(services: string[]): Promise<void> {
const smartconfigData = await readSmartconfigFile();
setCliConfigValueInData(smartconfigData, "services", services);
await writeSmartconfigFile(smartconfigData);
}
async function handleShowConfig(mode: ICliMode) {
const configData = await readServicesConfig();
if (mode.json) {
printJson(configData);
return;
}
helpers.printHeader("Current Services Configuration");
logger.log(
"info",
`Enabled Services: ${configData.enabledServices.length > 0 ? configData.enabledServices.join(", ") : "none configured"}`,
);
console.log();
if (!configData.environment) {
logger.log(
"note",
"No .nogit/env.json found yet. Start a service once to create runtime defaults.",
);
return;
}
const env = configData.environment;
logger.log("note", "MongoDB:");
logger.log("info", ` Host: ${env.MONGODB_HOST}:${env.MONGODB_PORT}`);
logger.log("info", ` Database: ${env.MONGODB_NAME}`);
logger.log("info", ` User: ${env.MONGODB_USER}`);
logger.log("info", ` Container: ${env.PROJECT_NAME}-mongodb`);
logger.log(
"info",
` Data: ${plugins.path.join(process.cwd(), ".nogit", "mongodata")}`,
);
logger.log("info", ` Connection: ${env.MONGODB_URL}`);
console.log();
logger.log("note", "S3/MinIO:");
logger.log("info", ` Host: ${env.S3_HOST}`);
logger.log("info", ` API Port: ${env.S3_PORT}`);
logger.log("info", ` Console Port: ${env.S3_CONSOLE_PORT}`);
logger.log("info", ` Bucket: ${env.S3_BUCKET}`);
logger.log("info", ` Container: ${env.PROJECT_NAME}-minio`);
logger.log(
"info",
` Data: ${plugins.path.join(process.cwd(), ".nogit", "miniodata")}`,
);
logger.log("info", ` Endpoint: ${env.S3_ENDPOINT}`);
console.log();
logger.log("note", "Elasticsearch:");
logger.log(
"info",
` Host: ${env.ELASTICSEARCH_HOST}:${env.ELASTICSEARCH_PORT}`,
);
logger.log("info", ` User: ${env.ELASTICSEARCH_USER}`);
logger.log("info", ` Container: ${env.PROJECT_NAME}-elasticsearch`);
logger.log(
"info",
` Data: ${plugins.path.join(process.cwd(), ".nogit", "esdata")}`,
);
logger.log("info", ` Connection: ${env.ELASTICSEARCH_URL}`);
}
async function handleSetServices(rawValue: string | undefined, mode: ICliMode) {
if (!rawValue) {
throw new Error("Specify a comma-separated list of services");
}
const requestedServices = rawValue
.split(",")
.map((service) => normalizeServiceName(service.trim()))
.filter(Boolean);
validateRequestedServices(requestedServices);
await updateEnabledServices(requestedServices);
if (mode.json) {
printJson({ ok: true, action: "set", enabledServices: requestedServices });
return;
}
logger.log("ok", `Enabled services set to: ${requestedServices.join(", ")}`);
}
async function handleEnableServices(
requestedServices: string[],
mode: ICliMode,
) {
const normalizedServices = requestedServices.map((service) =>
normalizeServiceName(service),
);
validateRequestedServices(normalizedServices);
const configData = await readServicesConfig();
const nextServices = Array.from(
new Set([...configData.enabledServices, ...normalizedServices]),
);
await updateEnabledServices(nextServices);
if (mode.json) {
printJson({ ok: true, action: "enable", enabledServices: nextServices });
return;
}
logger.log("ok", `Enabled services: ${nextServices.join(", ")}`);
}
async function handleDisableServices(
requestedServices: string[],
mode: ICliMode,
) {
const normalizedServices = requestedServices.map((service) =>
normalizeServiceName(service),
);
validateRequestedServices(normalizedServices);
const configData = await readServicesConfig();
const nextServices = configData.enabledServices.filter(
(service) => !normalizedServices.includes(service),
);
await updateEnabledServices(nextServices);
if (mode.json) {
printJson({ ok: true, action: "disable", enabledServices: nextServices });
return;
}
logger.log("ok", `Enabled services: ${nextServices.join(", ")}`);
}
function validateRequestedServices(services: string[]): void {
if (services.length === 0) {
throw new Error("Specify at least one service");
}
const invalidServices = services.filter(
(service) => !allowedServices.includes(service),
);
if (invalidServices.length > 0) {
throw new Error(`Unknown service(s): ${invalidServices.join(", ")}`);
}
}
async function handleStart(serviceManager: ServiceManager, service: string) { async function handleStart(serviceManager: ServiceManager, service: string) {
helpers.printHeader('Starting Services'); helpers.printHeader("Starting Services");
switch (service) { switch (service) {
case 'mongo': case "mongo":
case 'mongodb': case "mongodb":
await serviceManager.startMongoDB(); await serviceManager.startMongoDB();
break; break;
case 'minio': case "minio":
case 's3': case "s3":
await serviceManager.startMinIO(); await serviceManager.startMinIO();
break; break;
case 'elasticsearch': case "elasticsearch":
case 'es': case "es":
await serviceManager.startElasticsearch(); await serviceManager.startElasticsearch();
break; break;
case 'all': case "all":
case '': case "":
await serviceManager.startAll(); await serviceManager.startAll();
break; break;
default: default:
logger.log('error', `Unknown service: ${service}`); logger.log("error", `Unknown service: ${service}`);
logger.log('note', 'Use: mongo, s3, elasticsearch, or all'); logger.log("note", "Use: mongo, s3, elasticsearch, or all");
break; break;
} }
} }
async function handleStop(serviceManager: ServiceManager, service: string) { async function handleStop(serviceManager: ServiceManager, service: string) {
helpers.printHeader('Stopping Services'); helpers.printHeader("Stopping Services");
switch (service) { switch (service) {
case 'mongo': case "mongo":
case 'mongodb': case "mongodb":
await serviceManager.stopMongoDB(); await serviceManager.stopMongoDB();
break; break;
case 'minio': case "minio":
case 's3': case "s3":
await serviceManager.stopMinIO(); await serviceManager.stopMinIO();
break; break;
case 'elasticsearch': case "elasticsearch":
case 'es': case "es":
await serviceManager.stopElasticsearch(); await serviceManager.stopElasticsearch();
break; break;
case 'all': case "all":
case '': case "":
await serviceManager.stopAll(); await serviceManager.stopAll();
break; break;
default: default:
logger.log('error', `Unknown service: ${service}`); logger.log("error", `Unknown service: ${service}`);
logger.log('note', 'Use: mongo, s3, elasticsearch, or all'); logger.log("note", "Use: mongo, s3, elasticsearch, or all");
break; break;
} }
} }
async function handleRestart(serviceManager: ServiceManager, service: string) { async function handleRestart(serviceManager: ServiceManager, service: string) {
helpers.printHeader('Restarting Services'); helpers.printHeader("Restarting Services");
switch (service) { switch (service) {
case 'mongo': case "mongo":
case 'mongodb': case "mongodb":
await serviceManager.stopMongoDB(); await serviceManager.stopMongoDB();
await plugins.smartdelay.delayFor(2000); await plugins.smartdelay.delayFor(2000);
await serviceManager.startMongoDB(); await serviceManager.startMongoDB();
break; break;
case 'minio': case "minio":
case 's3': case "s3":
await serviceManager.stopMinIO(); await serviceManager.stopMinIO();
await plugins.smartdelay.delayFor(2000); await plugins.smartdelay.delayFor(2000);
await serviceManager.startMinIO(); await serviceManager.startMinIO();
break; break;
case 'elasticsearch': case "elasticsearch":
case 'es': case "es":
await serviceManager.stopElasticsearch(); await serviceManager.stopElasticsearch();
await plugins.smartdelay.delayFor(2000); await plugins.smartdelay.delayFor(2000);
await serviceManager.startElasticsearch(); await serviceManager.startElasticsearch();
break; break;
case 'all': case "all":
case '': case "":
await serviceManager.stopAll(); await serviceManager.stopAll();
await plugins.smartdelay.delayFor(2000); await plugins.smartdelay.delayFor(2000);
await serviceManager.startAll(); await serviceManager.startAll();
break; break;
default: default:
logger.log('error', `Unknown service: ${service}`); logger.log("error", `Unknown service: ${service}`);
break; break;
} }
} }
async function handleRemove(serviceManager: ServiceManager) { async function handleRemove(serviceManager: ServiceManager) {
helpers.printHeader('Removing Containers'); helpers.printHeader("Removing Containers");
logger.log('note', '⚠️ This will remove containers but preserve data'); logger.log("note", "⚠️ This will remove containers but preserve data");
const shouldContinue = await plugins.smartinteract.SmartInteract.getCliConfirmation('Continue?', false); const shouldContinue =
await plugins.smartinteract.SmartInteract.getCliConfirmation(
"Continue?",
false,
);
if (shouldContinue) { if (shouldContinue) {
await serviceManager.removeContainers(); await serviceManager.removeContainers();
} else { } else {
logger.log('note', 'Cancelled'); logger.log("note", "Cancelled");
} }
} }
async function handleClean(serviceManager: ServiceManager) { async function handleClean(serviceManager: ServiceManager) {
helpers.printHeader('Clean All'); helpers.printHeader("Clean All");
logger.log('error', '⚠️ WARNING: This will remove all containers and data!'); logger.log("error", "⚠️ WARNING: This will remove all containers and data!");
logger.log('error', 'This action cannot be undone!'); logger.log("error", "This action cannot be undone!");
const smartinteraction = new plugins.smartinteract.SmartInteract(); const smartinteraction = new plugins.smartinteract.SmartInteract();
const confirmAnswer = await smartinteraction.askQuestion({ const confirmAnswer = await smartinteraction.askQuestion({
name: 'confirm', name: "confirm",
type: 'input', type: "input",
message: 'Type "yes" to confirm:', message: 'Type "yes" to confirm:',
default: 'no' default: "no",
}); });
if (confirmAnswer.value === 'yes') { if (confirmAnswer.value === "yes") {
await serviceManager.removeContainers(); await serviceManager.removeContainers();
console.log(); console.log();
await serviceManager.cleanData(); await serviceManager.cleanData();
logger.log('ok', 'All cleaned ✓'); logger.log("ok", "All cleaned ✓");
} else { } else {
logger.log('note', 'Cancelled'); logger.log("note", "Cancelled");
} }
} }
async function handleConfigureServices(serviceManager: ServiceManager) { async function handleConfigureServices(serviceManager: ServiceManager) {
helpers.printHeader('Configure Services'); helpers.printHeader("Configure Services");
await serviceManager.configureServices(); await serviceManager.configureServices();
} }
function showHelp() { export function showHelp(mode?: ICliMode) {
helpers.printHeader('GitZone Services Manager'); if (mode?.json) {
printJson({
command: "services",
usage: "gitzone services <command> [options]",
commands: [
{
name: "config",
description:
"Show configured services and any existing runtime env.json data",
},
{
name: "set <csv>",
description: "Set the enabled service list without prompts",
},
{
name: "enable <service...>",
description: "Enable one or more services without prompts",
},
{
name: "disable <service...>",
description: "Disable one or more services without prompts",
},
{ name: "start [service]", description: "Start services" },
{ name: "stop [service]", description: "Stop services" },
{ name: "status", description: "Show service status" },
],
examples: [
"gitzone services config --json",
"gitzone services set mongodb,minio",
"gitzone services enable elasticsearch",
],
});
return;
}
logger.log('ok', 'Usage: gitzone services [command] [options]'); helpers.printHeader("GitZone Services Manager");
logger.log("ok", "Usage: gitzone services [command] [options]");
console.log(); console.log();
logger.log('note', 'Commands:'); logger.log("note", "Commands:");
logger.log('info', ' start [service] Start services (mongo|s3|elasticsearch|all)'); logger.log(
logger.log('info', ' stop [service] Stop services (mongo|s3|elasticsearch|all)'); "info",
logger.log('info', ' restart [service] Restart services (mongo|s3|elasticsearch|all)'); " start [service] Start services (mongo|s3|elasticsearch|all)",
logger.log('info', ' status Show service status'); );
logger.log('info', ' config Show current configuration'); logger.log(
logger.log('info', ' config services Configure which services are enabled'); "info",
logger.log('info', ' compass Show MongoDB Compass connection string'); " stop [service] Stop services (mongo|s3|elasticsearch|all)",
logger.log('info', ' logs [service] Show logs (mongo|s3|elasticsearch|all) [lines]'); );
logger.log('info', ' reconfigure Reassign ports and restart services'); logger.log(
logger.log('info', ' remove Remove all containers'); "info",
logger.log('info', ' clean Remove all containers and data ⚠️'); " restart [service] Restart services (mongo|s3|elasticsearch|all)",
logger.log('info', ' help Show this help message'); );
logger.log("info", " status Show service status");
logger.log("info", " config Show current configuration");
logger.log(
"info",
" config services Configure which services are enabled",
);
logger.log(
"info",
" set <csv> Set enabled services without prompts",
);
logger.log("info", " enable <svc...> Enable one or more services");
logger.log("info", " disable <svc...> Disable one or more services");
logger.log(
"info",
" compass Show MongoDB Compass connection string",
);
logger.log(
"info",
" logs [service] Show logs (mongo|s3|elasticsearch|all) [lines]",
);
logger.log("info", " reconfigure Reassign ports and restart services");
logger.log("info", " remove Remove all containers");
logger.log("info", " clean Remove all containers and data ⚠️");
logger.log("info", " help Show this help message");
console.log(); console.log();
logger.log('note', 'Available Services:'); logger.log("note", "Available Services:");
logger.log('info', ' • MongoDB (mongo) - Document database'); logger.log("info", " • MongoDB (mongo) - Document database");
logger.log('info', ' • MinIO (s3) - S3-compatible object storage'); logger.log("info", " • MinIO (s3) - S3-compatible object storage");
logger.log('info', ' • Elasticsearch (elasticsearch) - Search and analytics engine'); logger.log(
"info",
" • Elasticsearch (elasticsearch) - Search and analytics engine",
);
console.log(); console.log();
logger.log('note', 'Features:'); logger.log("note", "Features:");
logger.log('info', ' • Auto-creates .nogit/env.json with smart defaults'); logger.log("info", " • Auto-creates .nogit/env.json with smart defaults");
logger.log('info', ' • Random ports (20000-30000) for MongoDB/MinIO to avoid conflicts'); logger.log(
logger.log('info', ' • Elasticsearch uses standard port 9200'); "info",
logger.log('info', ' • Project-specific containers for multi-project support'); " • Random ports (20000-30000) for MongoDB/MinIO to avoid conflicts",
logger.log('info', ' • Preserves custom configuration values'); );
logger.log('info', 'MongoDB Compass connection support'); logger.log("info", "Elasticsearch uses standard port 9200");
logger.log(
"info",
" • Project-specific containers for multi-project support",
);
logger.log("info", " • Preserves custom configuration values");
logger.log("info", " • MongoDB Compass connection support");
console.log(); console.log();
logger.log('note', 'Examples:'); logger.log("note", "Examples:");
logger.log('info', ' gitzone services start # Start all services'); logger.log(
logger.log('info', ' gitzone services start mongo # Start only MongoDB'); "info",
logger.log('info', ' gitzone services start elasticsearch # Start only Elasticsearch'); " gitzone services start # Start all services",
logger.log('info', ' gitzone services stop # Stop all services'); );
logger.log('info', ' gitzone services status # Check service status'); logger.log(
logger.log('info', ' gitzone services config # Show configuration'); "info",
logger.log('info', ' gitzone services compass # Get MongoDB Compass connection'); " gitzone services start mongo # Start only MongoDB",
logger.log('info', ' gitzone services logs elasticsearch # Show Elasticsearch logs'); );
logger.log(
"info",
" gitzone services start elasticsearch # Start only Elasticsearch",
);
logger.log(
"info",
" gitzone services stop # Stop all services",
);
logger.log(
"info",
" gitzone services status # Check service status",
);
logger.log(
"info",
" gitzone services config # Show configuration",
);
logger.log(
"info",
" gitzone services config --json # Show configuration as JSON",
);
logger.log(
"info",
" gitzone services set mongodb,minio # Configure services without prompts",
);
logger.log(
"info",
" gitzone services compass # Get MongoDB Compass connection",
);
logger.log(
"info",
" gitzone services logs elasticsearch # Show Elasticsearch logs",
);
console.log(); console.log();
logger.log('note', 'Global Commands (-g/--global):'); logger.log("note", "Global Commands (-g/--global):");
logger.log('info', ' list -g List all registered projects'); logger.log("info", " list -g List all registered projects");
logger.log('info', ' status -g Show status across all projects'); logger.log("info", " status -g Show status across all projects");
logger.log('info', ' stop -g Stop all containers across all projects'); logger.log(
logger.log('info', ' cleanup -g Remove stale registry entries'); "info",
" stop -g Stop all containers across all projects",
);
logger.log("info", " cleanup -g Remove stale registry entries");
console.log(); console.log();
logger.log('note', 'Global Examples:'); logger.log("note", "Global Examples:");
logger.log('info', ' gitzone services list -g # List all registered projects'); logger.log(
logger.log('info', ' gitzone services status -g # Show global container status'); "info",
logger.log('info', ' gitzone services stop -g # Stop all (prompts for confirmation)'); " gitzone services list -g # List all registered projects",
);
logger.log(
"info",
" gitzone services status -g # Show global container status",
);
logger.log(
"info",
" gitzone services stop -g # Stop all (prompts for confirmation)",
);
} }
// ==================== Global Command Handlers ==================== // ==================== Global Command Handlers ====================
@@ -280,23 +627,23 @@ async function handleGlobalCommand(command: string) {
const globalRegistry = GlobalRegistry.getInstance(); const globalRegistry = GlobalRegistry.getInstance();
switch (command) { switch (command) {
case 'list': case "list":
await handleGlobalList(globalRegistry); await handleGlobalList(globalRegistry);
break; break;
case 'status': case "status":
await handleGlobalStatus(globalRegistry); await handleGlobalStatus(globalRegistry);
break; break;
case 'stop': case "stop":
await handleGlobalStop(globalRegistry); await handleGlobalStop(globalRegistry);
break; break;
case 'cleanup': case "cleanup":
await handleGlobalCleanup(globalRegistry); await handleGlobalCleanup(globalRegistry);
break; break;
case 'help': case "help":
default: default:
showHelp(); showHelp();
break; break;
@@ -304,13 +651,13 @@ async function handleGlobalCommand(command: string) {
} }
async function handleGlobalList(globalRegistry: GlobalRegistry) { async function handleGlobalList(globalRegistry: GlobalRegistry) {
helpers.printHeader('Registered Projects (Global)'); helpers.printHeader("Registered Projects (Global)");
const projects = await globalRegistry.getAllProjects(); const projects = await globalRegistry.getAllProjects();
const projectPaths = Object.keys(projects); const projectPaths = Object.keys(projects);
if (projectPaths.length === 0) { if (projectPaths.length === 0) {
logger.log('note', 'No projects registered'); logger.log("note", "No projects registered");
return; return;
} }
@@ -319,20 +666,20 @@ async function handleGlobalList(globalRegistry: GlobalRegistry) {
const lastActive = new Date(project.lastActive).toLocaleString(); const lastActive = new Date(project.lastActive).toLocaleString();
console.log(); console.log();
logger.log('ok', `📁 ${project.projectName}`); logger.log("ok", `📁 ${project.projectName}`);
logger.log('info', ` Path: ${project.projectPath}`); logger.log("info", ` Path: ${project.projectPath}`);
logger.log('info', ` Services: ${project.enabledServices.join(', ')}`); logger.log("info", ` Services: ${project.enabledServices.join(", ")}`);
logger.log('info', ` Last Active: ${lastActive}`); logger.log("info", ` Last Active: ${lastActive}`);
} }
} }
async function handleGlobalStatus(globalRegistry: GlobalRegistry) { async function handleGlobalStatus(globalRegistry: GlobalRegistry) {
helpers.printHeader('Global Service Status'); helpers.printHeader("Global Service Status");
const statuses = await globalRegistry.getGlobalStatus(); const statuses = await globalRegistry.getGlobalStatus();
if (statuses.length === 0) { if (statuses.length === 0) {
logger.log('note', 'No projects registered'); logger.log("note", "No projects registered");
return; return;
} }
@@ -341,28 +688,39 @@ async function handleGlobalStatus(globalRegistry: GlobalRegistry) {
for (const project of statuses) { for (const project of statuses) {
console.log(); console.log();
logger.log('ok', `📁 ${project.projectName}`); logger.log("ok", `📁 ${project.projectName}`);
logger.log('info', ` Path: ${project.projectPath}`); logger.log("info", ` Path: ${project.projectPath}`);
if (project.containers.length === 0) { if (project.containers.length === 0) {
logger.log('note', ' No containers configured'); logger.log("note", " No containers configured");
continue; continue;
} }
for (const container of project.containers) { for (const container of project.containers) {
totalContainers++; totalContainers++;
const statusIcon = container.status === 'running' ? '🟢' : container.status === 'exited' ? '🟡' : '⚪'; const statusIcon =
if (container.status === 'running') runningCount++; container.status === "running"
logger.log('info', ` ${statusIcon} ${container.name}: ${container.status}`); ? "🟢"
: container.status === "exited"
? "🟡"
: "⚪";
if (container.status === "running") runningCount++;
logger.log(
"info",
` ${statusIcon} ${container.name}: ${container.status}`,
);
} }
} }
console.log(); console.log();
logger.log('note', `Summary: ${runningCount}/${totalContainers} containers running across ${statuses.length} project(s)`); logger.log(
"note",
`Summary: ${runningCount}/${totalContainers} containers running across ${statuses.length} project(s)`,
);
} }
async function handleGlobalStop(globalRegistry: GlobalRegistry) { async function handleGlobalStop(globalRegistry: GlobalRegistry) {
helpers.printHeader('Stop All Containers (Global)'); helpers.printHeader("Stop All Containers (Global)");
const statuses = await globalRegistry.getGlobalStatus(); const statuses = await globalRegistry.getGlobalStatus();
@@ -370,64 +728,73 @@ async function handleGlobalStop(globalRegistry: GlobalRegistry) {
let runningCount = 0; let runningCount = 0;
for (const project of statuses) { for (const project of statuses) {
for (const container of project.containers) { for (const container of project.containers) {
if (container.status === 'running') runningCount++; if (container.status === "running") runningCount++;
} }
} }
if (runningCount === 0) { if (runningCount === 0) {
logger.log('note', 'No running containers found'); logger.log("note", "No running containers found");
return; return;
} }
logger.log('note', `Found ${runningCount} running container(s) across ${statuses.length} project(s)`); logger.log(
"note",
`Found ${runningCount} running container(s) across ${statuses.length} project(s)`,
);
console.log(); console.log();
// Show what will be stopped // Show what will be stopped
for (const project of statuses) { for (const project of statuses) {
const runningContainers = project.containers.filter(c => c.status === 'running'); const runningContainers = project.containers.filter(
(c) => c.status === "running",
);
if (runningContainers.length > 0) { if (runningContainers.length > 0) {
logger.log('info', `${project.projectName}:`); logger.log("info", `${project.projectName}:`);
for (const container of runningContainers) { for (const container of runningContainers) {
logger.log('info', `${container.name}`); logger.log("info", `${container.name}`);
} }
} }
} }
console.log(); console.log();
const shouldContinue = await plugins.smartinteract.SmartInteract.getCliConfirmation( const shouldContinue =
'Stop all containers?', await plugins.smartinteract.SmartInteract.getCliConfirmation(
false "Stop all containers?",
); false,
);
if (!shouldContinue) { if (!shouldContinue) {
logger.log('note', 'Cancelled'); logger.log("note", "Cancelled");
return; return;
} }
logger.log('note', 'Stopping all containers...'); logger.log("note", "Stopping all containers...");
const result = await globalRegistry.stopAll(); const result = await globalRegistry.stopAll();
if (result.stopped.length > 0) { if (result.stopped.length > 0) {
logger.log('ok', `Stopped: ${result.stopped.join(', ')}`); logger.log("ok", `Stopped: ${result.stopped.join(", ")}`);
} }
if (result.failed.length > 0) { if (result.failed.length > 0) {
logger.log('error', `Failed to stop: ${result.failed.join(', ')}`); logger.log("error", `Failed to stop: ${result.failed.join(", ")}`);
} }
} }
async function handleGlobalCleanup(globalRegistry: GlobalRegistry) { async function handleGlobalCleanup(globalRegistry: GlobalRegistry) {
helpers.printHeader('Cleanup Registry (Global)'); helpers.printHeader("Cleanup Registry (Global)");
logger.log('note', 'Checking for stale registry entries...'); logger.log("note", "Checking for stale registry entries...");
const removed = await globalRegistry.cleanup(); const removed = await globalRegistry.cleanup();
if (removed.length === 0) { if (removed.length === 0) {
logger.log('ok', 'No stale entries found'); logger.log("ok", "No stale entries found");
return; return;
} }
logger.log('ok', `Removed ${removed.length} stale entr${removed.length === 1 ? 'y' : 'ies'}:`); logger.log(
"ok",
`Removed ${removed.length} stale entr${removed.length === 1 ? "y" : "ies"}:`,
);
for (const path of removed) { for (const path of removed) {
logger.log('info', `${path}`); logger.log("info", `${path}`);
} }
} }
+191 -58
View File
@@ -1,91 +1,224 @@
/* ----------------------------------------------- /* -----------------------------------------------
* executes as standard task * executes as standard task
* ----------------------------------------------- */ * ----------------------------------------------- */
import * as plugins from './mod.plugins.js'; import * as plugins from "./mod.plugins.js";
import * as paths from '../paths.js'; import * as paths from "../paths.js";
import type { ICliMode } from "../helpers.climode.js";
import { getCliMode, printJson } from "../helpers.climode.js";
import { logger } from '../gitzone.logging.js'; import { logger } from "../gitzone.logging.js";
export let run = async () => { type ICommandHelpSummary = {
console.log(''); name: string;
console.log('╭─────────────────────────────────────────────────────────────╮'); description: string;
console.log('│ gitzone - Development Workflow CLI │'); };
console.log('╰─────────────────────────────────────────────────────────────╯');
console.log(''); const commandSummaries: ICommandHelpSummary[] = [
{
name: "commit",
description:
"Create semantic commits or generate read-only commit recommendations",
},
{ name: "format", description: "Plan or apply project formatting changes" },
{ name: "config", description: "Read and change .smartconfig.json settings" },
{ name: "services", description: "Manage or configure development services" },
{ name: "template", description: "Create a project from a template" },
{ name: "open", description: "Open project assets and CI pages" },
{ name: "docker", description: "Run Docker-related maintenance tasks" },
{
name: "deprecate",
description: "Deprecate npm packages across registries",
},
{ name: "meta", description: "Run meta-repository commands" },
{ name: "start", description: "Prepare a project for local work" },
{ name: "helpers", description: "Run helper utilities" },
];
export let run = async (argvArg: any = {}) => {
const mode = await getCliMode(argvArg);
const requestedCommandHelp =
argvArg._?.[0] === "help" ? argvArg._?.[1] : undefined;
if (mode.help || requestedCommandHelp) {
await showHelp(mode, requestedCommandHelp);
return;
}
if (!mode.interactive) {
await showHelp(mode);
return;
}
console.log("");
console.log(
"╭─────────────────────────────────────────────────────────────╮",
);
console.log(
"│ gitzone - Development Workflow CLI │",
);
console.log(
"╰─────────────────────────────────────────────────────────────╯",
);
console.log("");
const interactInstance = new plugins.smartinteract.SmartInteract(); const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({ const response = await interactInstance.askQuestion({
type: 'list', type: "list",
name: 'action', name: "action",
message: 'What would you like to do?', message: "What would you like to do?",
default: 'commit', default: "commit",
choices: [ choices: [
{ name: 'Commit changes (semantic versioning)', value: 'commit' }, { name: "Commit changes (semantic versioning)", value: "commit" },
{ name: 'Format project files', value: 'format' }, { name: "Format project files", value: "format" },
{ name: 'Configure release settings', value: 'config' }, { name: "Configure release settings", value: "config" },
{ name: 'Create from template', value: 'template' }, { name: "Create from template", value: "template" },
{ name: 'Manage dev services (MongoDB, S3)', value: 'services' }, { name: "Manage dev services (MongoDB, S3)", value: "services" },
{ name: 'Open project assets', value: 'open' }, { name: "Open project assets", value: "open" },
{ name: 'Show help', value: 'help' }, { name: "Show help", value: "help" },
], ],
}); });
const action = (response as any).value; const action = (response as any).value;
switch (action) { switch (action) {
case 'commit': { case "commit": {
const modCommit = await import('../mod_commit/index.js'); const modCommit = await import("../mod_commit/index.js");
await modCommit.run({ _: ['commit'] }); await modCommit.run({ _: ["commit"] });
break; break;
} }
case 'format': { case "format": {
const modFormat = await import('../mod_format/index.js'); const modFormat = await import("../mod_format/index.js");
await modFormat.run({ interactive: true }); await modFormat.run({ interactive: true });
break; break;
} }
case 'config': { case "config": {
const modConfig = await import('../mod_config/index.js'); const modConfig = await import("../mod_config/index.js");
await modConfig.run({ _: ['config'] }); await modConfig.run({ _: ["config"] });
break; break;
} }
case 'template': { case "template": {
const modTemplate = await import('../mod_template/index.js'); const modTemplate = await import("../mod_template/index.js");
await modTemplate.run({ _: ['template'] }); await modTemplate.run({ _: ["template"] });
break; break;
} }
case 'services': { case "services": {
const modServices = await import('../mod_services/index.js'); const modServices = await import("../mod_services/index.js");
await modServices.run({ _: ['services'] }); await modServices.run({ _: ["services"] });
break; break;
} }
case 'open': { case "open": {
const modOpen = await import('../mod_open/index.js'); const modOpen = await import("../mod_open/index.js");
await modOpen.run({ _: ['open'] }); await modOpen.run({ _: ["open"] });
break; break;
} }
case 'help': case "help":
showHelp(); await showHelp(mode);
break; break;
} }
}; };
function showHelp(): void { export async function showHelp(
console.log(''); mode: ICliMode,
console.log('Usage: gitzone <command> [options]'); commandName?: string,
console.log(''); ): Promise<void> {
console.log('Commands:'); if (commandName) {
console.log(' commit Create a semantic commit with versioning'); const handled = await showCommandHelp(commandName, mode);
console.log(' format Format and standardize project files'); if (handled) {
console.log(' config Manage release registry configuration'); return;
console.log(' template Create a new project from template'); }
console.log(' services Manage dev services (MongoDB, S3/MinIO)'); }
console.log(' open Open project assets (GitLab, npm, etc.)');
console.log(' docker Docker-related operations'); if (mode.json) {
console.log(' deprecate Deprecate a package on npm'); printJson({
console.log(' meta Run meta commands'); name: "gitzone",
console.log(' start Start working on a project'); usage: "gitzone <command> [options]",
console.log(' helpers Run helper utilities'); commands: commandSummaries,
console.log(''); globalFlags: [
console.log('Run gitzone <command> --help for more information on a command.'); { flag: "--help, -h", description: "Show help output" },
console.log(''); {
flag: "--json",
description: "Emit machine-readable JSON when supported",
},
{
flag: "--plain",
description: "Use plain text output when supported",
},
{
flag: "--agent",
description: "Prefer non-interactive machine-friendly output",
},
{
flag: "--no-interactive",
description: "Disable prompts and interactive menus",
},
{
flag: "--no-check-updates",
description: "Skip the update check banner",
},
],
});
return;
}
console.log("");
console.log("Usage: gitzone <command> [options]");
console.log("");
console.log("Commands:");
for (const commandSummary of commandSummaries) {
console.log(
` ${commandSummary.name.padEnd(11)} ${commandSummary.description}`,
);
}
console.log("");
console.log("Global flags:");
console.log(" --help, -h Show help output");
console.log(
" --json Emit machine-readable JSON when supported",
);
console.log(" --plain Use plain text output when supported");
console.log(
" --agent Prefer non-interactive machine-friendly output",
);
console.log(" --no-interactive Disable prompts and interactive menus");
console.log(" --no-check-updates Skip the update check banner");
console.log("");
console.log("Examples:");
console.log(" gitzone help commit");
console.log(" gitzone config show --json");
console.log(" gitzone commit recommend --json");
console.log(" gitzone format plan --json");
console.log(" gitzone services set mongodb,minio");
console.log("");
console.log("Run gitzone <command> --help for command-specific usage.");
console.log("");
}
async function showCommandHelp(
commandName: string,
mode: ICliMode,
): Promise<boolean> {
switch (commandName) {
case "commit": {
const modCommit = await import("../mod_commit/index.js");
modCommit.showHelp(mode);
return true;
}
case "config": {
const modConfig = await import("../mod_config/index.js");
modConfig.showHelp(mode);
return true;
}
case "format": {
const modFormat = await import("../mod_format/index.js");
modFormat.showHelp(mode);
return true;
}
case "services": {
const modServices = await import("../mod_services/index.js");
modServices.showHelp(mode);
return true;
}
default:
return false;
}
} }