diff --git a/oclif.manifest.json b/oclif.manifest.json index aee63ed..97b591a 100644 --- a/oclif.manifest.json +++ b/oclif.manifest.json @@ -617,6 +617,56 @@ "isESM": true, "relativePath": ["dist", "commands", "init.js"] }, + "issue": { + "aliases": [], + "args": { + "id": { + "description": "Issue ID (UUID or display number)", + "name": "id", + "required": true + } + }, + "description": "Alias for `get issue`. Get an issue by identifier", + "examples": [ + "<%= config.bin %> issue 1", + "<%= config.bin %> issue abc123-uuid", + "<%= config.bin %> issue abc123-uuid --json", + "<%= config.bin %> issue abc123-uuid --global", + "<%= config.bin %> issue 1 --project centy-daemon" + ], + "flags": { + "json": { + "description": "Output as JSON", + "name": "json", + "allowNo": false, + "type": "boolean" + }, + "global": { + "char": "g", + "description": "Search across all tracked projects", + "name": "global", + "allowNo": false, + "type": "boolean" + }, + "project": { + "description": "Project name or path (defaults to current directory)", + "name": "project", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "issue", + "pluginAlias": "centy", + "pluginName": "centy", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "isESM": true, + "relativePath": ["dist", "commands", "issue.js"] + }, "link": { "aliases": [], "args": { @@ -1420,6 +1470,127 @@ "isESM": true, "relativePath": ["dist", "commands", "add", "asset.js"] }, + "daemon:autostart": { + "aliases": [], + "args": {}, + "description": "Configure daemon to start automatically on boot", + "examples": [ + "<%= config.bin %> daemon autostart", + "<%= config.bin %> daemon autostart --enable", + "<%= config.bin %> daemon autostart --disable" + ], + "flags": { + "enable": { + "description": "Enable autostart on boot", + "exclusive": ["disable"], + "name": "enable", + "allowNo": false, + "type": "boolean" + }, + "disable": { + "description": "Disable autostart on boot", + "exclusive": ["enable"], + "name": "disable", + "allowNo": false, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "daemon:autostart", + "pluginAlias": "centy", + "pluginName": "centy", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "isESM": true, + "relativePath": ["dist", "commands", "daemon", "autostart.js"] + }, + "daemon:status": { + "aliases": [], + "args": {}, + "description": "Check whether the centy daemon is running", + "examples": [ + "<%= config.bin %> daemon status", + "<%= config.bin %> daemon status --json" + ], + "flags": { + "json": { + "description": "Output as JSON", + "name": "json", + "allowNo": false, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "daemon:status", + "pluginAlias": "centy", + "pluginName": "centy", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "isESM": true, + "relativePath": ["dist", "commands", "daemon", "status.js"] + }, + "delete:asset": { + "aliases": [], + "args": { + "filename": { + "description": "Asset filename", + "name": "filename", + "required": true + } + }, + "description": "Delete an asset", + "examples": [ + "<%= config.bin %> delete asset screenshot.png --issue 1", + "<%= config.bin %> delete asset logo.svg --shared", + "<%= config.bin %> delete asset old-image.png --issue 1 --force", + "<%= config.bin %> delete asset screenshot.png --issue 1 --project centy-daemon" + ], + "flags": { + "issue": { + "char": "i", + "description": "Issue ID or display number", + "name": "issue", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "shared": { + "char": "s", + "description": "Delete a shared asset", + "name": "shared", + "allowNo": false, + "type": "boolean" + }, + "force": { + "char": "f", + "description": "Skip confirmation prompt", + "name": "force", + "allowNo": false, + "type": "boolean" + }, + "project": { + "description": "Project name or path (defaults to current directory)", + "name": "project", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "delete:asset", + "pluginAlias": "centy", + "pluginName": "centy", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "isESM": true, + "relativePath": ["dist", "commands", "delete", "asset.js"] + }, "comment:add": { "aliases": [], "args": { @@ -1609,8 +1780,8 @@ "isESM": true, "relativePath": ["dist", "commands", "comment", "update.js"] }, - "delete:asset": { - "aliases": [], + "get:asset": { + "aliases": ["show:asset"], "args": { "filename": { "description": "Asset filename", @@ -1618,12 +1789,11 @@ "required": true } }, - "description": "Delete an asset", + "description": "Get an asset and save it to a file", "examples": [ - "<%= config.bin %> delete asset screenshot.png --issue 1", - "<%= config.bin %> delete asset logo.svg --shared", - "<%= config.bin %> delete asset old-image.png --issue 1 --force", - "<%= config.bin %> delete asset screenshot.png --issue 1 --project centy-daemon" + "<%= config.bin %> get asset screenshot.png --issue 1 --output ./screenshot.png", + "<%= config.bin %> get asset logo.svg --shared --output ./logo.svg", + "<%= config.bin %> get asset screenshot.png --issue 1 --project centy-daemon" ], "flags": { "issue": { @@ -1636,17 +1806,18 @@ }, "shared": { "char": "s", - "description": "Delete a shared asset", + "description": "Get a shared asset", "name": "shared", "allowNo": false, "type": "boolean" }, - "force": { - "char": "f", - "description": "Skip confirmation prompt", - "name": "force", - "allowNo": false, - "type": "boolean" + "output": { + "char": "o", + "description": "Output file path (defaults to asset filename)", + "name": "output", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, "project": { "description": "Project name or path (defaults to current directory)", @@ -1658,58 +1829,29 @@ }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "delete:asset", + "id": "get:asset", "pluginAlias": "centy", "pluginName": "centy", "pluginType": "core", "strict": true, "enableJsonFlag": false, "isESM": true, - "relativePath": ["dist", "commands", "delete", "asset.js"] + "relativePath": ["dist", "commands", "get", "asset.js"] }, - "daemon:autostart": { - "aliases": [], - "args": {}, - "description": "Configure daemon to start automatically on boot", - "examples": [ - "<%= config.bin %> daemon autostart", - "<%= config.bin %> daemon autostart --enable", - "<%= config.bin %> daemon autostart --disable" - ], - "flags": { - "enable": { - "description": "Enable autostart on boot", - "exclusive": ["disable"], - "name": "enable", - "allowNo": false, - "type": "boolean" - }, - "disable": { - "description": "Disable autostart on boot", - "exclusive": ["enable"], - "name": "disable", - "allowNo": false, - "type": "boolean" + "get:project": { + "aliases": ["show:project"], + "args": { + "path": { + "description": "Path to the project (defaults to current directory)", + "name": "path", + "required": false } }, - "hasDynamicHelp": false, - "hiddenAliases": [], - "id": "daemon:autostart", - "pluginAlias": "centy", - "pluginName": "centy", - "pluginType": "core", - "strict": true, - "enableJsonFlag": false, - "isESM": true, - "relativePath": ["dist", "commands", "daemon", "autostart.js"] - }, - "daemon:status": { - "aliases": [], - "args": {}, - "description": "Check whether the centy daemon is running", + "description": "Get info about a specific project", "examples": [ - "<%= config.bin %> daemon status", - "<%= config.bin %> daemon status --json" + "<%= config.bin %> get project", + "<%= config.bin %> get project /path/to/project", + "<%= config.bin %> get project --json" ], "flags": { "json": { @@ -1721,106 +1863,14 @@ }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "daemon:status", + "id": "get:project", "pluginAlias": "centy", "pluginName": "centy", "pluginType": "core", "strict": true, "enableJsonFlag": false, "isESM": true, - "relativePath": ["dist", "commands", "daemon", "status.js"] - }, - "get:asset": { - "aliases": ["show:asset"], - "args": { - "filename": { - "description": "Asset filename", - "name": "filename", - "required": true - } - }, - "description": "Get an asset and save it to a file", - "examples": [ - "<%= config.bin %> get asset screenshot.png --issue 1 --output ./screenshot.png", - "<%= config.bin %> get asset logo.svg --shared --output ./logo.svg", - "<%= config.bin %> get asset screenshot.png --issue 1 --project centy-daemon" - ], - "flags": { - "issue": { - "char": "i", - "description": "Issue ID or display number", - "name": "issue", - "hasDynamicHelp": false, - "multiple": false, - "type": "option" - }, - "shared": { - "char": "s", - "description": "Get a shared asset", - "name": "shared", - "allowNo": false, - "type": "boolean" - }, - "output": { - "char": "o", - "description": "Output file path (defaults to asset filename)", - "name": "output", - "hasDynamicHelp": false, - "multiple": false, - "type": "option" - }, - "project": { - "description": "Project name or path (defaults to current directory)", - "name": "project", - "hasDynamicHelp": false, - "multiple": false, - "type": "option" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [], - "id": "get:asset", - "pluginAlias": "centy", - "pluginName": "centy", - "pluginType": "core", - "strict": true, - "enableJsonFlag": false, - "isESM": true, - "relativePath": ["dist", "commands", "get", "asset.js"] - }, - "get:project": { - "aliases": ["show:project"], - "args": { - "path": { - "description": "Path to the project (defaults to current directory)", - "name": "path", - "required": false - } - }, - "description": "Get info about a specific project", - "examples": [ - "<%= config.bin %> get project", - "<%= config.bin %> get project /path/to/project", - "<%= config.bin %> get project --json" - ], - "flags": { - "json": { - "description": "Output as JSON", - "name": "json", - "allowNo": false, - "type": "boolean" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [], - "id": "get:project", - "pluginAlias": "centy", - "pluginName": "centy", - "pluginType": "core", - "strict": true, - "enableJsonFlag": false, - "isESM": true, - "relativePath": ["dist", "commands", "get", "project.js"] + "relativePath": ["dist", "commands", "get", "project.js"] }, "item-type:create": { "aliases": [], @@ -1943,152 +1993,105 @@ "isESM": true, "relativePath": ["dist", "commands", "item-type", "list.js"] }, - "list:assets": { - "aliases": ["asset:list"], - "args": {}, - "description": "List assets for an issue or shared assets", + "org:create": { + "aliases": [], + "args": { + "name": { + "description": "Organization display name", + "name": "name", + "required": true + } + }, + "description": "Create a new organization to group projects", "examples": [ - "<%= config.bin %> list assets --issue 1", - "<%= config.bin %> list assets --shared", - "<%= config.bin %> list assets --issue 1 --include-shared", - "<%= config.bin %> list assets --issue 1 --project centy-daemon" + "<%= config.bin %> org create \"My Company\"", + "<%= config.bin %> org create \"Centy.io\" --slug centy-io", + "<%= config.bin %> org create \"My Org\" --description \"Official projects\"" ], "flags": { - "issue": { - "char": "i", - "description": "Issue ID or display number", - "name": "issue", + "slug": { + "char": "s", + "description": "Custom slug (auto-generated from name if not provided)", + "name": "slug", "hasDynamicHelp": false, "multiple": false, "type": "option" }, - "shared": { - "char": "s", - "description": "List only shared assets", - "name": "shared", - "allowNo": false, - "type": "boolean" - }, - "include-shared": { - "description": "Include shared assets when listing issue assets", - "name": "include-shared", - "allowNo": false, - "type": "boolean" + "description": { + "char": "d", + "description": "Organization description", + "name": "description", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, "json": { "description": "Output as JSON", "name": "json", "allowNo": false, "type": "boolean" - }, - "project": { - "description": "Project name or path (defaults to current directory)", - "name": "project", - "hasDynamicHelp": false, - "multiple": false, - "type": "option" } }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "list:assets", + "id": "org:create", "pluginAlias": "centy", "pluginName": "centy", "pluginType": "core", "strict": true, "enableJsonFlag": false, "isESM": true, - "relativePath": ["dist", "commands", "list", "assets.js"] + "relativePath": ["dist", "commands", "org", "create.js"] }, - "list:items": { - "aliases": [], + "org:delete": { + "aliases": ["delete:org", "delete:organization"], "args": { - "type": { - "description": "Item type (e.g., issues, docs, bugs)", - "name": "type", + "slug": { + "description": "Organization slug", + "name": "slug", "required": true } }, - "description": "List items of any type", + "description": "Delete an organization (must have no projects assigned)", "examples": [ - "<%= config.bin %> list issues", - "<%= config.bin %> list issues --status open --priority 1", - "<%= config.bin %> list docs --json --limit 10", - "<%= config.bin %> list bugs --project centy-daemon", - "<%= config.bin %> list issues --global", - "<%= config.bin %> list issues --global --status open --json" + "<%= config.bin %> org delete my-org --force", + "<%= config.bin %> delete org my-org --force", + "<%= config.bin %> delete organization old-org --force" ], "flags": { - "status": { - "description": "Filter by status. Prefix with ! to exclude (e.g., !closed)", - "name": "status", - "hasDynamicHelp": false, - "multiple": false, - "type": "option" - }, - "priority": { - "description": "Filter by priority level", - "name": "priority", - "hasDynamicHelp": false, - "multiple": false, - "type": "option" - }, - "limit": { - "description": "Limit number of results (0 = no limit)", - "name": "limit", - "default": 0, - "hasDynamicHelp": false, - "multiple": false, - "type": "option" - }, - "offset": { - "description": "Offset for pagination", - "name": "offset", - "default": 0, - "hasDynamicHelp": false, - "multiple": false, - "type": "option" - }, - "json": { - "description": "Output as JSON", - "name": "json", - "allowNo": false, - "type": "boolean" - }, - "project": { - "description": "Project name or path (defaults to current directory)", - "name": "project", - "hasDynamicHelp": false, - "multiple": false, - "type": "option" - }, - "global": { - "char": "g", - "description": "Query items across all tracked projects", - "name": "global", + "force": { + "char": "f", + "description": "Skip confirmation prompt", + "name": "force", "allowNo": false, "type": "boolean" } }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "list:items", + "id": "org:delete", "pluginAlias": "centy", "pluginName": "centy", "pluginType": "core", "strict": true, "enableJsonFlag": false, "isESM": true, - "relativePath": ["dist", "commands", "list", "items.js"] + "relativePath": ["dist", "commands", "org", "delete.js"] }, - "list:link-types": { - "aliases": [], - "args": {}, - "description": "List all available link types", + "org:get": { + "aliases": ["get:org", "get:organization"], + "args": { + "slug": { + "description": "Organization slug", + "name": "slug", + "required": true + } + }, + "description": "Get organization details by slug", "examples": [ - "<%= config.bin %> list link-types", - "<%= config.bin %> list link-types --json", - "<%= config.bin %> list link-types --project centy-daemon" + "<%= config.bin %> org get centy-io", + "<%= config.bin %> get org centy-io", + "<%= config.bin %> get organization my-org --json" ], "flags": { "json": { @@ -2096,71 +2099,30 @@ "name": "json", "allowNo": false, "type": "boolean" - }, - "project": { - "description": "Project name or path (defaults to current directory)", - "name": "project", - "hasDynamicHelp": false, - "multiple": false, - "type": "option" } }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "list:link-types", + "id": "org:get", "pluginAlias": "centy", "pluginName": "centy", "pluginType": "core", "strict": true, "enableJsonFlag": false, "isESM": true, - "relativePath": ["dist", "commands", "list", "link-types.js"] + "relativePath": ["dist", "commands", "org", "get.js"] }, - "list:projects": { - "aliases": ["project:list"], + "org:list": { + "aliases": ["list:orgs", "list:organizations"], "args": {}, - "description": "List all tracked centy projects", + "description": "List all organizations", "examples": [ - "<%= config.bin %> list projects", - "<%= config.bin %> list projects --include-stale", - "<%= config.bin %> list projects --include-uninitialized", - "<%= config.bin %> list projects --include-temp", - "<%= config.bin %> list projects --org centy-io", - "<%= config.bin %> list projects --ungrouped", - "<%= config.bin %> list projects --json" + "<%= config.bin %> org list", + "<%= config.bin %> list orgs", + "<%= config.bin %> list organizations", + "<%= config.bin %> org list --json" ], "flags": { - "include-stale": { - "description": "Include projects where path no longer exists", - "name": "include-stale", - "allowNo": false, - "type": "boolean" - }, - "include-uninitialized": { - "description": "Include projects that are not initialized", - "name": "include-uninitialized", - "allowNo": false, - "type": "boolean" - }, - "include-temp": { - "description": "Include projects in system temp directory", - "name": "include-temp", - "allowNo": false, - "type": "boolean" - }, - "org": { - "description": "Filter by organization slug", - "name": "org", - "hasDynamicHelp": false, - "multiple": false, - "type": "option" - }, - "ungrouped": { - "description": "Only show projects without an organization", - "name": "ungrouped", - "allowNo": false, - "type": "boolean" - }, "json": { "description": "Output as JSON", "name": "json", @@ -2170,47 +2132,60 @@ }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "list:projects", + "id": "org:list", "pluginAlias": "centy", "pluginName": "centy", "pluginType": "core", "strict": true, "enableJsonFlag": false, "isESM": true, - "relativePath": ["dist", "commands", "list", "projects.js"] + "relativePath": ["dist", "commands", "org", "list.js"] }, - "org:create": { - "aliases": [], + "org:update": { + "aliases": [ + "update:org", + "update:organization", + "edit:org", + "edit:organization" + ], "args": { - "name": { - "description": "Organization display name", - "name": "name", + "slug": { + "description": "Organization slug", + "name": "slug", "required": true } }, - "description": "Create a new organization to group projects", + "description": "Update an organization", "examples": [ - "<%= config.bin %> org create \"My Company\"", - "<%= config.bin %> org create \"Centy.io\" --slug centy-io", - "<%= config.bin %> org create \"My Org\" --description \"Official projects\"" + "<%= config.bin %> org update my-org --name \"New Name\"", + "<%= config.bin %> org update my-org --description \"Updated description\"", + "<%= config.bin %> org update my-org --new-slug new-slug", + "<%= config.bin %> update org centy-io --name \"Centy.io\"" ], "flags": { - "slug": { - "char": "s", - "description": "Custom slug (auto-generated from name if not provided)", - "name": "slug", + "name": { + "char": "n", + "description": "New organization name", + "name": "name", "hasDynamicHelp": false, "multiple": false, "type": "option" }, "description": { "char": "d", - "description": "Organization description", + "description": "New organization description", "name": "description", "hasDynamicHelp": false, "multiple": false, "type": "option" }, + "new-slug": { + "description": "Rename the organization slug", + "name": "new-slug", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, "json": { "description": "Output as JSON", "name": "json", @@ -2220,93 +2195,161 @@ }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "org:create", + "id": "org:update", "pluginAlias": "centy", "pluginName": "centy", "pluginType": "core", "strict": true, "enableJsonFlag": false, "isESM": true, - "relativePath": ["dist", "commands", "org", "create.js"] + "relativePath": ["dist", "commands", "org", "update.js"] }, - "org:delete": { - "aliases": ["delete:org", "delete:organization"], - "args": { - "slug": { - "description": "Organization slug", - "name": "slug", - "required": true - } - }, - "description": "Delete an organization (must have no projects assigned)", + "list:assets": { + "aliases": ["asset:list"], + "args": {}, + "description": "List assets for an issue or shared assets", "examples": [ - "<%= config.bin %> org delete my-org --force", - "<%= config.bin %> delete org my-org --force", - "<%= config.bin %> delete organization old-org --force" + "<%= config.bin %> list assets --issue 1", + "<%= config.bin %> list assets --shared", + "<%= config.bin %> list assets --issue 1 --include-shared", + "<%= config.bin %> list assets --issue 1 --project centy-daemon" ], "flags": { - "force": { - "char": "f", - "description": "Skip confirmation prompt", - "name": "force", + "issue": { + "char": "i", + "description": "Issue ID or display number", + "name": "issue", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "shared": { + "char": "s", + "description": "List only shared assets", + "name": "shared", "allowNo": false, "type": "boolean" + }, + "include-shared": { + "description": "Include shared assets when listing issue assets", + "name": "include-shared", + "allowNo": false, + "type": "boolean" + }, + "json": { + "description": "Output as JSON", + "name": "json", + "allowNo": false, + "type": "boolean" + }, + "project": { + "description": "Project name or path (defaults to current directory)", + "name": "project", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "org:delete", + "id": "list:assets", "pluginAlias": "centy", "pluginName": "centy", "pluginType": "core", "strict": true, "enableJsonFlag": false, "isESM": true, - "relativePath": ["dist", "commands", "org", "delete.js"] + "relativePath": ["dist", "commands", "list", "assets.js"] }, - "org:get": { - "aliases": ["get:org", "get:organization"], + "list:items": { + "aliases": [], "args": { - "slug": { - "description": "Organization slug", - "name": "slug", + "type": { + "description": "Item type (e.g., issues, docs, bugs)", + "name": "type", "required": true } }, - "description": "Get organization details by slug", + "description": "List items of any type", "examples": [ - "<%= config.bin %> org get centy-io", - "<%= config.bin %> get org centy-io", - "<%= config.bin %> get organization my-org --json" + "<%= config.bin %> list issues", + "<%= config.bin %> list issues --status open --priority 1", + "<%= config.bin %> list docs --json --limit 10", + "<%= config.bin %> list bugs --project centy-daemon", + "<%= config.bin %> list issues --global", + "<%= config.bin %> list issues --global --status open --json" ], "flags": { + "status": { + "description": "Filter by status. Prefix with ! to exclude (e.g., !closed)", + "name": "status", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "priority": { + "description": "Filter by priority level", + "name": "priority", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "limit": { + "description": "Limit number of results (0 = no limit)", + "name": "limit", + "default": 0, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "offset": { + "description": "Offset for pagination", + "name": "offset", + "default": 0, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, "json": { "description": "Output as JSON", "name": "json", "allowNo": false, "type": "boolean" + }, + "project": { + "description": "Project name or path (defaults to current directory)", + "name": "project", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "global": { + "char": "g", + "description": "Query items across all tracked projects", + "name": "global", + "allowNo": false, + "type": "boolean" } }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "org:get", + "id": "list:items", "pluginAlias": "centy", "pluginName": "centy", "pluginType": "core", "strict": true, "enableJsonFlag": false, "isESM": true, - "relativePath": ["dist", "commands", "org", "get.js"] + "relativePath": ["dist", "commands", "list", "items.js"] }, - "org:list": { - "aliases": ["list:orgs", "list:organizations"], + "list:link-types": { + "aliases": [], "args": {}, - "description": "List all organizations", + "description": "List all available link types", "examples": [ - "<%= config.bin %> org list", - "<%= config.bin %> list orgs", - "<%= config.bin %> list organizations", - "<%= config.bin %> org list --json" + "<%= config.bin %> list link-types", + "<%= config.bin %> list link-types --json", + "<%= config.bin %> list link-types --project centy-daemon" ], "flags": { "json": { @@ -2314,64 +2357,71 @@ "name": "json", "allowNo": false, "type": "boolean" + }, + "project": { + "description": "Project name or path (defaults to current directory)", + "name": "project", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "org:list", + "id": "list:link-types", "pluginAlias": "centy", "pluginName": "centy", "pluginType": "core", "strict": true, "enableJsonFlag": false, "isESM": true, - "relativePath": ["dist", "commands", "org", "list.js"] + "relativePath": ["dist", "commands", "list", "link-types.js"] }, - "org:update": { - "aliases": [ - "update:org", - "update:organization", - "edit:org", - "edit:organization" - ], - "args": { - "slug": { - "description": "Organization slug", - "name": "slug", - "required": true - } - }, - "description": "Update an organization", + "list:projects": { + "aliases": ["project:list"], + "args": {}, + "description": "List all tracked centy projects", "examples": [ - "<%= config.bin %> org update my-org --name \"New Name\"", - "<%= config.bin %> org update my-org --description \"Updated description\"", - "<%= config.bin %> org update my-org --new-slug new-slug", - "<%= config.bin %> update org centy-io --name \"Centy.io\"" + "<%= config.bin %> list projects", + "<%= config.bin %> list projects --include-stale", + "<%= config.bin %> list projects --include-uninitialized", + "<%= config.bin %> list projects --include-temp", + "<%= config.bin %> list projects --org centy-io", + "<%= config.bin %> list projects --ungrouped", + "<%= config.bin %> list projects --json" ], "flags": { - "name": { - "char": "n", - "description": "New organization name", - "name": "name", - "hasDynamicHelp": false, - "multiple": false, - "type": "option" + "include-stale": { + "description": "Include projects where path no longer exists", + "name": "include-stale", + "allowNo": false, + "type": "boolean" }, - "description": { - "char": "d", - "description": "New organization description", - "name": "description", - "hasDynamicHelp": false, - "multiple": false, - "type": "option" + "include-uninitialized": { + "description": "Include projects that are not initialized", + "name": "include-uninitialized", + "allowNo": false, + "type": "boolean" }, - "new-slug": { - "description": "Rename the organization slug", - "name": "new-slug", + "include-temp": { + "description": "Include projects in system temp directory", + "name": "include-temp", + "allowNo": false, + "type": "boolean" + }, + "org": { + "description": "Filter by organization slug", + "name": "org", "hasDynamicHelp": false, "multiple": false, "type": "option" }, + "ungrouped": { + "description": "Only show projects without an organization", + "name": "ungrouped", + "allowNo": false, + "type": "boolean" + }, "json": { "description": "Output as JSON", "name": "json", @@ -2381,14 +2431,14 @@ }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "org:update", + "id": "list:projects", "pluginAlias": "centy", "pluginName": "centy", "pluginType": "core", "strict": true, "enableJsonFlag": false, "isESM": true, - "relativePath": ["dist", "commands", "org", "update.js"] + "relativePath": ["dist", "commands", "list", "projects.js"] }, "project:archive": { "aliases": [], @@ -2644,76 +2694,6 @@ "isESM": true, "relativePath": ["dist", "commands", "sync", "users.js"] }, - "untrack:org": { - "aliases": ["untrack:organization"], - "args": { - "slug": { - "description": "Organization slug", - "name": "slug", - "required": true - } - }, - "description": "Remove an organization from tracking", - "examples": [ - "<%= config.bin %> untrack org my-org", - "<%= config.bin %> untrack org my-org --force", - "<%= config.bin %> untrack organization my-org" - ], - "flags": { - "force": { - "char": "f", - "description": "Skip confirmation prompt", - "name": "force", - "allowNo": false, - "type": "boolean" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [], - "id": "untrack:org", - "pluginAlias": "centy", - "pluginName": "centy", - "pluginType": "core", - "strict": true, - "enableJsonFlag": false, - "isESM": true, - "relativePath": ["dist", "commands", "untrack", "org.js"] - }, - "untrack:project": { - "aliases": [], - "args": { - "path": { - "description": "Path to the project (defaults to current directory)", - "name": "path", - "required": false - } - }, - "description": "Remove a project from tracking", - "examples": [ - "<%= config.bin %> untrack project", - "<%= config.bin %> untrack project /path/to/project", - "<%= config.bin %> untrack project --force" - ], - "flags": { - "force": { - "char": "f", - "description": "Skip confirmation prompt", - "name": "force", - "allowNo": false, - "type": "boolean" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [], - "id": "untrack:project", - "pluginAlias": "centy", - "pluginName": "centy", - "pluginType": "core", - "strict": true, - "enableJsonFlag": false, - "isESM": true, - "relativePath": ["dist", "commands", "untrack", "project.js"] - }, "user:create": { "aliases": [], "args": {}, @@ -2936,6 +2916,76 @@ "isESM": true, "relativePath": ["dist", "commands", "user", "update.js"] }, + "untrack:org": { + "aliases": ["untrack:organization"], + "args": { + "slug": { + "description": "Organization slug", + "name": "slug", + "required": true + } + }, + "description": "Remove an organization from tracking", + "examples": [ + "<%= config.bin %> untrack org my-org", + "<%= config.bin %> untrack org my-org --force", + "<%= config.bin %> untrack organization my-org" + ], + "flags": { + "force": { + "char": "f", + "description": "Skip confirmation prompt", + "name": "force", + "allowNo": false, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "untrack:org", + "pluginAlias": "centy", + "pluginName": "centy", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "isESM": true, + "relativePath": ["dist", "commands", "untrack", "org.js"] + }, + "untrack:project": { + "aliases": [], + "args": { + "path": { + "description": "Path to the project (defaults to current directory)", + "name": "path", + "required": false + } + }, + "description": "Remove a project from tracking", + "examples": [ + "<%= config.bin %> untrack project", + "<%= config.bin %> untrack project /path/to/project", + "<%= config.bin %> untrack project --force" + ], + "flags": { + "force": { + "char": "f", + "description": "Skip confirmation prompt", + "name": "force", + "allowNo": false, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "untrack:project", + "pluginAlias": "centy", + "pluginName": "centy", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "isESM": true, + "relativePath": ["dist", "commands", "untrack", "project.js"] + }, "workspace:cleanup": { "aliases": [], "args": {}, @@ -3151,5 +3201,5 @@ "relativePath": ["dist", "commands", "workspace", "open.js"] } }, - "version": "0.8.4" + "version": "0.8.5" } diff --git a/src/commands/issue.spec.ts b/src/commands/issue.spec.ts new file mode 100644 index 0000000..cdb14a5 --- /dev/null +++ b/src/commands/issue.spec.ts @@ -0,0 +1,289 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + createMockCommand, + runCommandSafely, +} from '../testing/command-test-utils.js' + +const mockDaemonGetItem = vi.fn() +const mockDaemonGetIssuesByUuid = vi.fn() +const mockSearchItemsByDisplayNumberGlobally = vi.fn() +const mockResolveProjectPath = vi.fn() +const mockEnsureInitialized = vi.fn() + +vi.mock('../daemon/daemon-get-item.js', () => ({ + daemonGetItem: (...args: unknown[]) => mockDaemonGetItem(...args), +})) + +vi.mock('../daemon/daemon-get-issues-by-uuid.js', () => ({ + daemonGetIssuesByUuid: (...args: unknown[]) => + mockDaemonGetIssuesByUuid(...args), +})) + +vi.mock('../daemon/daemon-search-items-globally.js', () => ({ + searchItemsByDisplayNumberGlobally: (...args: unknown[]) => + mockSearchItemsByDisplayNumberGlobally(...args), +})) + +vi.mock('../utils/resolve-project-path.js', () => ({ + resolveProjectPath: (...args: unknown[]) => mockResolveProjectPath(...args), +})) + +vi.mock('../utils/ensure-initialized.js', () => ({ + ensureInitialized: (...args: unknown[]) => mockEnsureInitialized(...args), + NotInitializedError: class NotInitializedError extends Error { + constructor(message = 'Not initialized') { + super(message) + this.name = 'NotInitializedError' + } + }, +})) + +vi.mock('../utils/cross-project-search.js', () => ({ + isValidUuid: vi.fn(id => + /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i.test(id) + ), +})) + +function createMockGenericItem(overrides: Record = {}) { + return { + id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + itemType: 'issues', + title: 'Test Issue', + body: 'Test description', + metadata: { + displayNumber: 1, + status: 'open', + priority: 1, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-02T00:00:00Z', + deletedAt: '', + customFields: {}, + }, + ...overrides, + } +} + +describe('Issue command (alias for get issue)', () => { + beforeEach(() => { + vi.clearAllMocks() + mockResolveProjectPath.mockResolvedValue('/test/project') + mockEnsureInitialized.mockResolvedValue('/test/project/.centy') + }) + + it('should get issue by display number', async () => { + const { default: Command } = await import('./issue.js') + const item = createMockGenericItem() + mockDaemonGetItem.mockResolvedValue({ success: true, item }) + + const cmd = createMockCommand(Command, { + flags: { json: false, global: false }, + args: { id: '1' }, + }) + + await cmd.run() + + expect(mockDaemonGetItem).toHaveBeenCalledWith({ + projectPath: '/test/project', + itemType: 'issues', + itemId: '', + displayNumber: 1, + }) + expect(cmd.logs.some(log => log.includes('Issue #1'))).toBe(true) + expect(cmd.logs.some(log => log.includes('Test Issue'))).toBe(true) + }) + + it('should get issue by UUID', async () => { + const { default: Command } = await import('./issue.js') + const item = createMockGenericItem() + mockDaemonGetItem.mockResolvedValue({ success: true, item }) + + const cmd = createMockCommand(Command, { + flags: { json: false, global: false }, + args: { id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' }, + }) + + await cmd.run() + + expect(mockDaemonGetItem).toHaveBeenCalledWith({ + projectPath: '/test/project', + itemType: 'issues', + itemId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + }) + }) + + it('should always use issues as item type', async () => { + const { default: Command } = await import('./issue.js') + const item = createMockGenericItem() + mockDaemonGetItem.mockResolvedValue({ success: true, item }) + + const cmd = createMockCommand(Command, { + flags: { json: false, global: false }, + args: { id: '1' }, + }) + + await cmd.run() + + expect(mockDaemonGetItem).toHaveBeenCalledWith( + expect.objectContaining({ itemType: 'issues' }) + ) + }) + + it('should output JSON when json flag is set', async () => { + const { default: Command } = await import('./issue.js') + const item = createMockGenericItem() + mockDaemonGetItem.mockResolvedValue({ success: true, item }) + + const cmd = createMockCommand(Command, { + flags: { json: true, global: false }, + args: { id: '1' }, + }) + + await cmd.run() + + expect(cmd.logs[0]).toBe(JSON.stringify(item, null, 2)) + }) + + it('should search globally with --global flag by UUID', async () => { + const { default: Command } = await import('./issue.js') + mockDaemonGetIssuesByUuid.mockResolvedValue({ + items: [ + { + item: { + id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + itemType: 'issues', + title: 'Test Issue', + body: '', + metadata: { + displayNumber: 1, + status: 'open', + priority: 1, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-02T00:00:00Z', + deletedAt: '', + customFields: {}, + }, + }, + projectName: 'project-a', + projectPath: '/path/to/project-a', + displayPath: '~/project-a', + }, + ], + totalCount: 1, + errors: [], + }) + + const cmd = createMockCommand(Command, { + flags: { json: false, global: true }, + args: { id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' }, + }) + + await cmd.run() + + expect(mockDaemonGetIssuesByUuid).toHaveBeenCalledWith({ + uuid: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + }) + expect(cmd.logs.some(log => log.includes('Found 1 issue(s)'))).toBe(true) + }) + + it('should search globally with --global flag by display number', async () => { + const { default: Command } = await import('./issue.js') + mockSearchItemsByDisplayNumberGlobally.mockResolvedValue({ + items: [ + { + item: { + id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + itemType: 'issues', + title: 'Test Issue', + body: '', + metadata: { + displayNumber: 1, + status: 'open', + priority: 1, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-02T00:00:00Z', + deletedAt: '', + customFields: {}, + }, + }, + projectName: 'project-a', + projectPath: '/path/to/project-a', + displayPath: '~/project-a', + }, + ], + errors: [], + }) + + const cmd = createMockCommand(Command, { + flags: { json: false, global: true }, + args: { id: '1' }, + }) + + await cmd.run() + + expect(mockSearchItemsByDisplayNumberGlobally).toHaveBeenCalledWith( + 'issues', + 1 + ) + expect(cmd.logs.some(log => log.includes('Found 1 issue(s)'))).toBe(true) + }) + + it('should handle daemon get item failure', async () => { + const { default: Command } = await import('./issue.js') + mockDaemonGetItem.mockResolvedValue({ + success: false, + error: 'Issue not found', + }) + + const cmd = createMockCommand(Command, { + flags: { json: false, global: false }, + args: { id: '999' }, + }) + + const { error } = await runCommandSafely(cmd) + + expect(error).toBeDefined() + expect(cmd.errors).toContain('Issue not found') + }) + + it('should handle NotInitializedError', async () => { + const { default: Command } = await import('./issue.js') + const { NotInitializedError } = + await import('../utils/ensure-initialized.js') + mockEnsureInitialized.mockRejectedValue( + new NotInitializedError('Project not initialized') + ) + + const cmd = createMockCommand(Command, { + flags: { json: false, global: false }, + args: { id: '1' }, + }) + + const { error } = await runCommandSafely(cmd) + + expect(error).toBeDefined() + expect(cmd.errors).toContain('Project not initialized') + }) + + it('should use project flag to resolve path', async () => { + const { default: Command } = await import('./issue.js') + mockResolveProjectPath.mockResolvedValue('/other/project') + const item = createMockGenericItem() + mockDaemonGetItem.mockResolvedValue({ success: true, item }) + + const cmd = createMockCommand(Command, { + flags: { json: false, global: false, project: 'other-project' }, + args: { id: '1' }, + }) + + await cmd.run() + + expect(mockResolveProjectPath).toHaveBeenCalledWith('other-project') + }) + + it('should have only id arg (no type arg)', async () => { + const { default: Command } = await import('./issue.js') + + expect(Object.keys(Command.args)).toEqual(['id']) + expect(Command.args).not.toHaveProperty('type') + }) +}) diff --git a/src/commands/issue.ts b/src/commands/issue.ts new file mode 100644 index 0000000..f535ffe --- /dev/null +++ b/src/commands/issue.ts @@ -0,0 +1,94 @@ +import { Args, Command, Flags } from '@oclif/core' +import { daemonGetItem } from '../daemon/daemon-get-item.js' +import { projectFlag } from '../flags/project-flag.js' +import { formatGenericItem } from '../lib/get-item/format-generic-item.js' +import { handleGlobalGet } from '../lib/get-item/handle-global-get.js' +import { parseDisplayNumber } from '../lib/resolve-item-id/parse-display-number.js' +import { + ensureInitialized, + NotInitializedError, +} from '../utils/ensure-initialized.js' +import { resolveProjectPath } from '../utils/resolve-project-path.js' + +/** + * Alias for `get issue`. Get an issue by identifier without specifying the type. + */ +export default class Issue extends Command { + static override args = { + id: Args.string({ + description: 'Issue ID (UUID or display number)', + required: true, + }), + } + + static override description = + 'Alias for `get issue`. Get an issue by identifier' + + static override examples = [ + '<%= config.bin %> issue 1', + '<%= config.bin %> issue abc123-uuid', + '<%= config.bin %> issue abc123-uuid --json', + '<%= config.bin %> issue abc123-uuid --global', + '<%= config.bin %> issue 1 --project centy-daemon', + ] + + static override flags = { + json: Flags.boolean({ + description: 'Output as JSON', + default: false, + }), + global: Flags.boolean({ + char: 'g', + description: 'Search across all tracked projects', + default: false, + }), + project: projectFlag, + } + + public async run(): Promise { + const { args, flags } = await this.parse(Issue) + const itemType = 'issues' + const cwd = await resolveProjectPath(flags.project) + + if (flags.global) { + const throwError = (msg: string): never => this.error(msg) + await handleGlobalGet( + itemType, + args.id, + flags.json, + this.log.bind(this), + this.warn.bind(this), + throwError + ) + return + } + + try { + await ensureInitialized(cwd) + } catch (error) { + if (error instanceof NotInitializedError) { + this.error(error.message) + } + throw error instanceof Error ? error : new Error(String(error)) + } + + const displayNumber = parseDisplayNumber(args.id) + const response = await daemonGetItem({ + projectPath: cwd, + itemType, + itemId: displayNumber !== undefined ? '' : args.id, + displayNumber, + }) + + if (!response.success) { + this.error(response.error) + } + + if (flags.json) { + this.log(JSON.stringify(response.item, null, 2)) + return + } + + formatGenericItem(response.item!, this.log.bind(this)) + } +}