@bussolabs/closeyourit-cli 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/base.d.ts +11 -0
- package/dist/base.js +19 -0
- package/dist/commands/matrix/categories/create.d.ts +13 -0
- package/dist/commands/matrix/categories/create.js +33 -0
- package/dist/commands/matrix/categories/delete.d.ts +13 -0
- package/dist/commands/matrix/categories/delete.js +27 -0
- package/dist/commands/matrix/categories/list.d.ts +12 -0
- package/dist/commands/matrix/categories/list.js +27 -0
- package/dist/commands/matrix/categories/update.d.ts +14 -0
- package/dist/commands/matrix/categories/update.js +37 -0
- package/dist/commands/matrix/cells/clear.d.ts +18 -0
- package/dist/commands/matrix/cells/clear.js +34 -0
- package/dist/commands/matrix/cells/set.d.ts +15 -0
- package/dist/commands/matrix/cells/set.js +42 -0
- package/dist/commands/matrix/features/create.d.ts +16 -0
- package/dist/commands/matrix/features/create.js +40 -0
- package/dist/commands/matrix/features/delete.d.ts +13 -0
- package/dist/commands/matrix/features/delete.js +27 -0
- package/dist/commands/matrix/features/list.d.ts +13 -0
- package/dist/commands/matrix/features/list.js +35 -0
- package/dist/commands/matrix/features/show.d.ts +10 -0
- package/dist/commands/matrix/features/show.js +33 -0
- package/dist/commands/matrix/features/update.d.ts +17 -0
- package/dist/commands/matrix/features/update.js +45 -0
- package/dist/commands/matrix/list.d.ts +9 -0
- package/dist/commands/matrix/list.js +18 -0
- package/dist/commands/matrix/show.d.ts +16 -0
- package/dist/commands/matrix/show.js +63 -0
- package/dist/commands/matrix/statuses.d.ts +6 -0
- package/dist/commands/matrix/statuses.js +22 -0
- package/dist/lib/api.d.ts +7 -0
- package/dist/lib/api.js +14 -1
- package/dist/lib/output.d.ts +8 -0
- package/dist/lib/output.js +20 -0
- package/oclif.manifest.json +4267 -3517
- package/package.json +5 -2
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base';
|
|
2
|
+
export default class MatrixList extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
page: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
7
|
+
};
|
|
8
|
+
run(): Promise<unknown>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const base_1 = require("../../base");
|
|
4
|
+
const output_1 = require("../../lib/output");
|
|
5
|
+
class MatrixList extends base_1.BaseCommand {
|
|
6
|
+
static description = 'List the products (groups) whose feature matrix you can see';
|
|
7
|
+
static examples = ['<%= config.bin %> matrix list', '<%= config.bin %> matrix list --page 2 --json'];
|
|
8
|
+
static flags = { ...base_1.pageFlag };
|
|
9
|
+
async run() {
|
|
10
|
+
const { flags } = await this.parse(MatrixList);
|
|
11
|
+
const res = await this.api.get(`/cli/v1/product/matrix?page=${flags.page}`);
|
|
12
|
+
if (!this.jsonEnabled()) {
|
|
13
|
+
this.log((0, output_1.renderTable)(['NAME', 'ID'], (res.data ?? []).map((product) => [String(product.name ?? ''), String(product.id ?? '')])));
|
|
14
|
+
}
|
|
15
|
+
return res;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.default = MatrixList;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base';
|
|
2
|
+
export default class MatrixShow extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
product: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
run(): Promise<unknown>;
|
|
9
|
+
/**
|
|
10
|
+
* The first column holds `Category/Feature` — the very reference `matrix cells set` and
|
|
11
|
+
* `matrix features` take, so a row can be acted on without a second lookup.
|
|
12
|
+
*/
|
|
13
|
+
private renderMatrix;
|
|
14
|
+
/** `-` = not set (different from an explicit "not applicable" status, which has its own code). */
|
|
15
|
+
private renderCell;
|
|
16
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const core_1 = require("@oclif/core");
|
|
4
|
+
const base_1 = require("../../base");
|
|
5
|
+
const output_1 = require("../../lib/output");
|
|
6
|
+
class MatrixShow extends base_1.BaseCommand {
|
|
7
|
+
static args = {
|
|
8
|
+
product: core_1.Args.string({ description: 'Product id (UUID) or group name', required: true }),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Show a product feature matrix: features in rows, platforms in columns';
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> matrix show CloseYourIt',
|
|
13
|
+
'<%= config.bin %> matrix show CloseYourIt --json',
|
|
14
|
+
];
|
|
15
|
+
async run() {
|
|
16
|
+
const { args } = await this.parse(MatrixShow);
|
|
17
|
+
const productId = await this.resolveGroupId(args.product);
|
|
18
|
+
const res = await this.api.get(`/cli/v1/product/matrix/${encodeURIComponent(productId)}`);
|
|
19
|
+
if (!this.jsonEnabled())
|
|
20
|
+
this.renderMatrix(res.data ?? {});
|
|
21
|
+
return res;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The first column holds `Category/Feature` — the very reference `matrix cells set` and
|
|
25
|
+
* `matrix features` take, so a row can be acted on without a second lookup.
|
|
26
|
+
*/
|
|
27
|
+
renderMatrix(matrix) {
|
|
28
|
+
const platforms = matrix.platforms ?? [];
|
|
29
|
+
if (platforms.length === 0) {
|
|
30
|
+
this.log('No platforms yet: declare them on the products projects, or write a cell to open a column.');
|
|
31
|
+
}
|
|
32
|
+
const byCell = new Map();
|
|
33
|
+
for (const cell of matrix.cells ?? [])
|
|
34
|
+
byCell.set(`${cell.feature_id}|${cell.platform_id}`, cell);
|
|
35
|
+
const rows = [];
|
|
36
|
+
for (const category of matrix.categories ?? []) {
|
|
37
|
+
for (const feature of category.features ?? []) {
|
|
38
|
+
rows.push([
|
|
39
|
+
`${category.name ?? ''}/${feature.name ?? ''}`,
|
|
40
|
+
...platforms.map((platform) => this.renderCell(byCell.get(`${feature.id}|${platform.id}`))),
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (rows.length === 0) {
|
|
45
|
+
this.log('No features yet. Add one with: matrix features create <product> --category <name> --name <name>');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.log((0, output_1.renderTable)(['FEATURE', ...platforms.map((platform) => String(platform.code ?? '').toUpperCase())], rows));
|
|
49
|
+
const missing = Number(matrix.missing_release_count ?? 0);
|
|
50
|
+
if (missing > 0) {
|
|
51
|
+
this.log(`\n${missing} released cell(s) without a version (marked "?"). Set one with matrix cells set --release.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** `-` = not set (different from an explicit "not applicable" status, which has its own code). */
|
|
55
|
+
renderCell(cell) {
|
|
56
|
+
if (!cell?.status?.code)
|
|
57
|
+
return '-';
|
|
58
|
+
if (cell.release?.version)
|
|
59
|
+
return `${cell.status.code} ${cell.release.version}`;
|
|
60
|
+
return cell.release_missing ? `${cell.status.code} ?` : String(cell.status.code);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.default = MatrixShow;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const base_1 = require("../../base");
|
|
4
|
+
const output_1 = require("../../lib/output");
|
|
5
|
+
class MatrixStatuses extends base_1.BaseCommand {
|
|
6
|
+
static description = 'List the cell statuses of your organization (the codes accepted by matrix cells set)';
|
|
7
|
+
static examples = ['<%= config.bin %> matrix statuses', '<%= config.bin %> matrix statuses --json'];
|
|
8
|
+
async run() {
|
|
9
|
+
await this.parse(MatrixStatuses);
|
|
10
|
+
const res = await this.api.get('/cli/v1/types/feature_statuses');
|
|
11
|
+
if (!this.jsonEnabled()) {
|
|
12
|
+
this.log((0, output_1.renderTable)(['CODE', 'LABEL', 'LIFECYCLE', 'ID'], (res.data ?? []).map((status) => [
|
|
13
|
+
String(status.code ?? ''),
|
|
14
|
+
String(status.label ?? ''),
|
|
15
|
+
String(status.category ?? ''),
|
|
16
|
+
String(status.id ?? ''),
|
|
17
|
+
])));
|
|
18
|
+
}
|
|
19
|
+
return res;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.default = MatrixStatuses;
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -25,6 +25,13 @@ export interface UploadFile {
|
|
|
25
25
|
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
26
26
|
export declare class CliApi {
|
|
27
27
|
private readonly config;
|
|
28
|
+
/**
|
|
29
|
+
* Meta of the most recent successful response envelope (`{page, per, total, total_pages}`, or
|
|
30
|
+
* undefined for non-paginated responses). `BaseCommand.finally` reads it to print the "there are
|
|
31
|
+
* more pages" footer once per command, instead of threading the envelope through all 30 list
|
|
32
|
+
* commands (CYCL-11). The instance is per-command, so this is that command's last response.
|
|
33
|
+
*/
|
|
34
|
+
lastMeta?: Record<string, unknown>;
|
|
28
35
|
constructor(config: CliConfig);
|
|
29
36
|
request<T = unknown>(method: HttpMethod, path: string, opts?: RequestOptions): Promise<Envelope<T>>;
|
|
30
37
|
/**
|
package/dist/lib/api.js
CHANGED
|
@@ -18,6 +18,13 @@ class ApiRequestError extends Error {
|
|
|
18
18
|
exports.ApiRequestError = ApiRequestError;
|
|
19
19
|
class CliApi {
|
|
20
20
|
config;
|
|
21
|
+
/**
|
|
22
|
+
* Meta of the most recent successful response envelope (`{page, per, total, total_pages}`, or
|
|
23
|
+
* undefined for non-paginated responses). `BaseCommand.finally` reads it to print the "there are
|
|
24
|
+
* more pages" footer once per command, instead of threading the envelope through all 30 list
|
|
25
|
+
* commands (CYCL-11). The instance is per-command, so this is that command's last response.
|
|
26
|
+
*/
|
|
27
|
+
lastMeta;
|
|
21
28
|
constructor(config) {
|
|
22
29
|
this.config = config;
|
|
23
30
|
}
|
|
@@ -97,7 +104,9 @@ class CliApi {
|
|
|
97
104
|
const error = envelope.error ?? {};
|
|
98
105
|
throw new ApiRequestError(res.status, error.code ?? `C${res.status}-API-000`, error.message ?? res.statusText ?? `Request failed (${res.status})`, error.details);
|
|
99
106
|
}
|
|
100
|
-
|
|
107
|
+
const envelope = (json ?? {});
|
|
108
|
+
this.lastMeta = envelope.meta;
|
|
109
|
+
return envelope;
|
|
101
110
|
}
|
|
102
111
|
get(path, opts) {
|
|
103
112
|
return this.request('GET', path, opts);
|
|
@@ -132,6 +141,10 @@ class CliApi {
|
|
|
132
141
|
await this.handleResponse(res); // always throws: maps the JSON error envelope
|
|
133
142
|
throw new ApiRequestError(res.status, `C${res.status}-API-000`, `Download failed (${res.status})`);
|
|
134
143
|
}
|
|
144
|
+
// A binary payload is not a paginated collection; clear any meta a prior lookup (e.g.
|
|
145
|
+
// resolveProjectId) left behind so it cannot leak into the pagination footer (CYCL-11). The
|
|
146
|
+
// success path bypasses handleResponse, so this reset must be explicit.
|
|
147
|
+
this.lastMeta = undefined;
|
|
135
148
|
const data = new Uint8Array(await res.arrayBuffer());
|
|
136
149
|
const disposition = res.headers.get('content-disposition') ?? '';
|
|
137
150
|
const match = /filename\*?=(?:UTF-8''|")?([^";]+)/i.exec(disposition);
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -15,6 +15,14 @@ export declare function sanitizeMultiline(value: unknown): string;
|
|
|
15
15
|
export declare function displayWidth(value: string): number;
|
|
16
16
|
/** Minimal dependency-free table renderer for human output. */
|
|
17
17
|
export declare function renderTable(headers: string[], rows: string[][]): string;
|
|
18
|
+
/**
|
|
19
|
+
* Footer line telling the reader that the human table is only one backend page of a larger set:
|
|
20
|
+
* without it, page 1 reads as the whole collection and a 900-ticket project looks like 10 (CYCL-11).
|
|
21
|
+
* Returns undefined when nothing more follows — a single page, the last page, or a collection already
|
|
22
|
+
* aggregated by `fetchAllPages` (whose last saved meta has `page === total_pages`) — so complete
|
|
23
|
+
* output stays clean. Callers gate on `!jsonEnabled()`, so machine output never sees this.
|
|
24
|
+
*/
|
|
25
|
+
export declare function paginationFooter(meta: unknown): string | undefined;
|
|
18
26
|
/**
|
|
19
27
|
* Status dots make problems scannable at a glance. Four fixed-width circles (no variation
|
|
20
28
|
* selectors, so table columns stay aligned): down=🔴, warn=🟡, ok=🟢, neutral=⚪.
|
package/dist/lib/output.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.sanitize = sanitize;
|
|
|
4
4
|
exports.sanitizeMultiline = sanitizeMultiline;
|
|
5
5
|
exports.displayWidth = displayWidth;
|
|
6
6
|
exports.renderTable = renderTable;
|
|
7
|
+
exports.paginationFooter = paginationFooter;
|
|
7
8
|
exports.statusDot = statusDot;
|
|
8
9
|
exports.statusCell = statusCell;
|
|
9
10
|
exports.section = section;
|
|
@@ -72,6 +73,25 @@ function renderTable(headers, rows) {
|
|
|
72
73
|
const separator = widths.map((width) => '-'.repeat(width));
|
|
73
74
|
return [line(safeHeaders), line(separator), ...safeRows.map(line)].join('\n');
|
|
74
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Footer line telling the reader that the human table is only one backend page of a larger set:
|
|
78
|
+
* without it, page 1 reads as the whole collection and a 900-ticket project looks like 10 (CYCL-11).
|
|
79
|
+
* Returns undefined when nothing more follows — a single page, the last page, or a collection already
|
|
80
|
+
* aggregated by `fetchAllPages` (whose last saved meta has `page === total_pages`) — so complete
|
|
81
|
+
* output stays clean. Callers gate on `!jsonEnabled()`, so machine output never sees this.
|
|
82
|
+
*/
|
|
83
|
+
function paginationFooter(meta) {
|
|
84
|
+
if (!meta || typeof meta !== 'object')
|
|
85
|
+
return undefined;
|
|
86
|
+
const { page, total, total_pages: totalPages } = meta;
|
|
87
|
+
const current = Number(page);
|
|
88
|
+
const last = Number(totalPages);
|
|
89
|
+
if (!Number.isInteger(current) || !Number.isInteger(last) || current < 1 || current >= last)
|
|
90
|
+
return undefined;
|
|
91
|
+
const count = Number(total);
|
|
92
|
+
const totalPart = Number.isFinite(count) ? ` — ${count} items total` : '';
|
|
93
|
+
return `Showing page ${current} of ${last}${totalPart}. Use --page ${current + 1} to see more.`;
|
|
94
|
+
}
|
|
75
95
|
const TONE_DOT = {
|
|
76
96
|
down: '🔴',
|
|
77
97
|
neutral: '⚪',
|