exponential-cli 1.1.0 → 1.4.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/dist/index.js +491 -30
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command13 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/auth.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -170,6 +170,29 @@ function createAuthCommand() {
|
|
|
170
170
|
handleError(error);
|
|
171
171
|
}
|
|
172
172
|
});
|
|
173
|
+
auth.command("show").description(
|
|
174
|
+
"Print parts of the current credentials. Use --token to print the raw JWT (for piping into other tools, e.g. `gh secret set EXPONENTIAL_TOKEN < <(exponential auth show --token)`)."
|
|
175
|
+
).option("--token", "Print the raw JWT to stdout with no trailing newline").action((options) => {
|
|
176
|
+
const config = getConfig();
|
|
177
|
+
if (options.token) {
|
|
178
|
+
if (!config.token) {
|
|
179
|
+
console.error("Not authenticated. Run `exponential auth login` first.");
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
process.stdout.write(config.token);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
console.log(chalk2.bold("\nCredentials"));
|
|
186
|
+
console.log(chalk2.gray("\u2500".repeat(40)));
|
|
187
|
+
console.log(`API URL: ${config.apiUrl || chalk2.gray("(not set)")}`);
|
|
188
|
+
console.log(
|
|
189
|
+
`Token: ${config.token ? chalk2.gray(`${config.token.substring(0, 20)}...`) : chalk2.gray("(not set)")}`
|
|
190
|
+
);
|
|
191
|
+
console.log(
|
|
192
|
+
chalk2.gray("Use `exponential auth show --token` to print the raw JWT.")
|
|
193
|
+
);
|
|
194
|
+
console.log();
|
|
195
|
+
});
|
|
173
196
|
auth.command("status").description("Show detailed authentication status").action(() => {
|
|
174
197
|
const config = getConfig();
|
|
175
198
|
console.log(chalk2.bold("\nAuthentication Status"));
|
|
@@ -768,6 +791,87 @@ function getFeatureStatusColor(status) {
|
|
|
768
791
|
return "gray";
|
|
769
792
|
}
|
|
770
793
|
}
|
|
794
|
+
function transformUserStory(story) {
|
|
795
|
+
return {
|
|
796
|
+
id: story.id,
|
|
797
|
+
featureId: story.featureId,
|
|
798
|
+
scopeId: story.scopeId,
|
|
799
|
+
asA: story.asA,
|
|
800
|
+
iWant: story.iWant,
|
|
801
|
+
soThat: story.soThat,
|
|
802
|
+
acceptanceCriteria: story.acceptanceCriteria,
|
|
803
|
+
displayOrder: story.displayOrder,
|
|
804
|
+
createdAt: new Date(story.createdAt).toISOString(),
|
|
805
|
+
updatedAt: new Date(story.updatedAt).toISOString()
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
function outputUserStoryJson(story) {
|
|
809
|
+
console.log(JSON.stringify(transformUserStory(story), null, 2));
|
|
810
|
+
}
|
|
811
|
+
function outputUserStoryPretty(story) {
|
|
812
|
+
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
813
|
+
const order = chalk3.gray(`#${story.displayOrder}`);
|
|
814
|
+
console.log(`
|
|
815
|
+
${order} ${chalk3.bold(story.iWant ?? '(no "I want")')}`);
|
|
816
|
+
console.log(chalk3.gray(` ID: ${story.id}`));
|
|
817
|
+
if (story.asA) console.log(` ${chalk3.cyan("As a:")} ${story.asA}`);
|
|
818
|
+
if (story.iWant) console.log(` ${chalk3.cyan("I want:")} ${story.iWant}`);
|
|
819
|
+
if (story.soThat) console.log(` ${chalk3.cyan("So that:")} ${story.soThat}`);
|
|
820
|
+
if (story.acceptanceCriteria) {
|
|
821
|
+
console.log(` ${chalk3.yellow("Acceptance:")} ${story.acceptanceCriteria}`);
|
|
822
|
+
}
|
|
823
|
+
if (story.scopeId) console.log(` ${chalk3.magenta("Scope:")} ${story.scopeId}`);
|
|
824
|
+
console.log();
|
|
825
|
+
}
|
|
826
|
+
function outputUserStoriesJson(stories) {
|
|
827
|
+
console.log(JSON.stringify({
|
|
828
|
+
userStories: stories.map(transformUserStory),
|
|
829
|
+
total: stories.length
|
|
830
|
+
}, null, 2));
|
|
831
|
+
}
|
|
832
|
+
function outputUserStoriesPretty(stories) {
|
|
833
|
+
if (stories.length === 0) {
|
|
834
|
+
console.log(chalk3.gray("No user stories found."));
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
console.log(chalk3.bold(`
|
|
838
|
+
User stories (${stories.length} total)`));
|
|
839
|
+
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
840
|
+
for (const story of stories) {
|
|
841
|
+
const order = chalk3.gray(`#${story.displayOrder}`);
|
|
842
|
+
const parts = [
|
|
843
|
+
story.asA ? `As a ${story.asA},` : null,
|
|
844
|
+
story.iWant ? `I want ${story.iWant},` : null,
|
|
845
|
+
story.soThat ? `so that ${story.soThat}.` : null
|
|
846
|
+
].filter(Boolean).join(" ");
|
|
847
|
+
console.log(` ${order} ${parts || chalk3.gray("(empty)")}`);
|
|
848
|
+
console.log(chalk3.gray(` ID: ${story.id}`));
|
|
849
|
+
}
|
|
850
|
+
console.log();
|
|
851
|
+
}
|
|
852
|
+
function outputBatchResultsJson(results) {
|
|
853
|
+
const succeeded = results.filter((r) => r.ok).length;
|
|
854
|
+
console.log(JSON.stringify({
|
|
855
|
+
results,
|
|
856
|
+
total: results.length,
|
|
857
|
+
succeeded,
|
|
858
|
+
failed: results.length - succeeded
|
|
859
|
+
}, null, 2));
|
|
860
|
+
}
|
|
861
|
+
function outputBatchResultsPretty(results) {
|
|
862
|
+
const succeeded = results.filter((r) => r.ok).length;
|
|
863
|
+
console.log(chalk3.bold(`
|
|
864
|
+
Added ${succeeded}/${results.length} user stories`));
|
|
865
|
+
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
866
|
+
for (const r of results) {
|
|
867
|
+
if (r.ok) {
|
|
868
|
+
console.log(` ${chalk3.green("\u2713")} [${r.index}] ${chalk3.gray(r.id ?? "")}`);
|
|
869
|
+
} else {
|
|
870
|
+
console.log(` ${chalk3.red("\u2717")} [${r.index}] ${r.error ?? "failed"}`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
console.log();
|
|
874
|
+
}
|
|
771
875
|
function outputTicketJson(ticket) {
|
|
772
876
|
console.log(JSON.stringify(transformTicket(ticket), null, 2));
|
|
773
877
|
}
|
|
@@ -1767,7 +1871,116 @@ function createEpicsCommand() {
|
|
|
1767
1871
|
}
|
|
1768
1872
|
|
|
1769
1873
|
// src/commands/features.ts
|
|
1874
|
+
import { Command as Command7 } from "commander";
|
|
1875
|
+
|
|
1876
|
+
// src/commands/stories.ts
|
|
1770
1877
|
import { Command as Command6 } from "commander";
|
|
1878
|
+
async function readStdin() {
|
|
1879
|
+
const chunks = [];
|
|
1880
|
+
for await (const chunk of process.stdin) {
|
|
1881
|
+
chunks.push(chunk);
|
|
1882
|
+
}
|
|
1883
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
1884
|
+
}
|
|
1885
|
+
function createStoriesCommand() {
|
|
1886
|
+
const stories = new Command6("stories").description(
|
|
1887
|
+
"Manage a feature's native, structured user stories (As a / I want / So that)."
|
|
1888
|
+
);
|
|
1889
|
+
stories.command("add").description(
|
|
1890
|
+
[
|
|
1891
|
+
"Add user stories to a feature.",
|
|
1892
|
+
"",
|
|
1893
|
+
"Flag mode: pass --as-a/--i-want/--so-that (plus optional --acceptance/--scope) to add one story.",
|
|
1894
|
+
"Batch mode: pipe a JSON array of { asA?, iWant?, soThat?, acceptanceCriteria?, scopeId? } on stdin",
|
|
1895
|
+
" to add many at once (per-item success/failure is reported)."
|
|
1896
|
+
].join("\n")
|
|
1897
|
+
).requiredOption("--feature <id>", "Feature CUID").option("--as-a <text>", '"As a ..." actor').option("--i-want <text>", '"I want ..." capability').option("--so-that <text>", '"So that ..." outcome').option("--acceptance <text>", "Acceptance criteria").option("--scope <id>", "Feature scope CUID to group the story under").action(
|
|
1898
|
+
async (options, cmd) => {
|
|
1899
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1900
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
1901
|
+
const hasFlags = options.asA != null || options.iWant != null || options.soThat != null || options.acceptance != null;
|
|
1902
|
+
const isBatch = !hasFlags && !process.stdin.isTTY;
|
|
1903
|
+
try {
|
|
1904
|
+
const client = getClient();
|
|
1905
|
+
if (isBatch) {
|
|
1906
|
+
const raw = await readStdin();
|
|
1907
|
+
let items;
|
|
1908
|
+
try {
|
|
1909
|
+
const parsed = JSON.parse(raw);
|
|
1910
|
+
if (!Array.isArray(parsed)) {
|
|
1911
|
+
throw new Error("Expected a JSON array of story objects on stdin");
|
|
1912
|
+
}
|
|
1913
|
+
items = parsed;
|
|
1914
|
+
} catch (parseError) {
|
|
1915
|
+
throw new Error(
|
|
1916
|
+
`Could not parse stdin as a JSON array: ${parseError instanceof Error ? parseError.message : String(parseError)}`
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
const results = [];
|
|
1920
|
+
for (let index = 0; index < items.length; index++) {
|
|
1921
|
+
const item = items[index];
|
|
1922
|
+
try {
|
|
1923
|
+
const story2 = await client.userStories.create({
|
|
1924
|
+
featureId: options.feature,
|
|
1925
|
+
asA: item.asA,
|
|
1926
|
+
iWant: item.iWant,
|
|
1927
|
+
soThat: item.soThat,
|
|
1928
|
+
acceptanceCriteria: item.acceptanceCriteria,
|
|
1929
|
+
scopeId: item.scopeId
|
|
1930
|
+
});
|
|
1931
|
+
results.push({ index, ok: true, id: story2.id });
|
|
1932
|
+
} catch (itemError) {
|
|
1933
|
+
results.push({
|
|
1934
|
+
index,
|
|
1935
|
+
ok: false,
|
|
1936
|
+
error: itemError instanceof Error ? itemError.message : String(itemError)
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
if (useJson) outputBatchResultsJson(results);
|
|
1941
|
+
else outputBatchResultsPretty(results);
|
|
1942
|
+
if (results.length > 0 && results.every((r) => !r.ok)) {
|
|
1943
|
+
process.exit(1);
|
|
1944
|
+
}
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
const input = {
|
|
1948
|
+
featureId: options.feature,
|
|
1949
|
+
asA: options.asA,
|
|
1950
|
+
iWant: options.iWant,
|
|
1951
|
+
soThat: options.soThat,
|
|
1952
|
+
acceptanceCriteria: options.acceptance,
|
|
1953
|
+
scopeId: options.scope
|
|
1954
|
+
};
|
|
1955
|
+
const story = await client.userStories.create(input);
|
|
1956
|
+
if (useJson) outputUserStoryJson(story);
|
|
1957
|
+
else {
|
|
1958
|
+
console.log("\n\u2713 User story added");
|
|
1959
|
+
outputUserStoryPretty(story);
|
|
1960
|
+
}
|
|
1961
|
+
} catch (error) {
|
|
1962
|
+
handleError(error, useJson);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
);
|
|
1966
|
+
stories.command("list").description("List a feature's user stories in display order").requiredOption("--feature <id>", "Feature CUID").action(
|
|
1967
|
+
async (options, cmd) => {
|
|
1968
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1969
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
1970
|
+
try {
|
|
1971
|
+
const client = getClient();
|
|
1972
|
+
const list = await client.userStories.list({ featureId: options.feature });
|
|
1973
|
+
if (useJson) outputUserStoriesJson(list);
|
|
1974
|
+
else outputUserStoriesPretty(list);
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
handleError(error, useJson);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
);
|
|
1980
|
+
return stories;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/commands/features.ts
|
|
1771
1984
|
var FEATURE_STATUSES = [
|
|
1772
1985
|
"IDEA",
|
|
1773
1986
|
"DEFINED",
|
|
@@ -1785,7 +1998,7 @@ function validateFeatureStatus(value) {
|
|
|
1785
1998
|
return value;
|
|
1786
1999
|
}
|
|
1787
2000
|
function createFeaturesCommand() {
|
|
1788
|
-
const features = new
|
|
2001
|
+
const features = new Command7("features").description(
|
|
1789
2002
|
"Manage features. A feature lives under a product and groups user stories, scopes, and tickets."
|
|
1790
2003
|
);
|
|
1791
2004
|
features.command("list").description("List features in a product").requiredOption("--product <slug|id>", "Product slug or CUID").option("--workspace <slug|id>", "Workspace (required when --product is a slug)").option("--status <status>", `Filter by status: ${FEATURE_STATUSES.join(", ")}`).action(
|
|
@@ -1879,13 +2092,171 @@ function createFeaturesCommand() {
|
|
|
1879
2092
|
}
|
|
1880
2093
|
}
|
|
1881
2094
|
);
|
|
2095
|
+
features.addCommand(createStoriesCommand());
|
|
1882
2096
|
return features;
|
|
1883
2097
|
}
|
|
1884
2098
|
|
|
2099
|
+
// src/commands/labels.ts
|
|
2100
|
+
import { Command as Command8 } from "commander";
|
|
2101
|
+
import chalk4 from "chalk";
|
|
2102
|
+
var TRIAGE_LABELS = [
|
|
2103
|
+
{ slug: "needs-triage", name: "needs-triage", color: "avatar-orange" },
|
|
2104
|
+
{ slug: "needs-info", name: "needs-info", color: "avatar-yellow" },
|
|
2105
|
+
{ slug: "ready-for-agent", name: "ready-for-agent", color: "avatar-green" },
|
|
2106
|
+
{ slug: "ready-for-human", name: "ready-for-human", color: "avatar-lightBlue" },
|
|
2107
|
+
{ slug: "wontfix", name: "wontfix", color: "avatar-lightGray" }
|
|
2108
|
+
];
|
|
2109
|
+
var CUID_PATTERN = /^c[a-z0-9]{20,}$/i;
|
|
2110
|
+
function looksLikeCuid(value) {
|
|
2111
|
+
return CUID_PATTERN.test(value);
|
|
2112
|
+
}
|
|
2113
|
+
function outputTagsPretty(tags) {
|
|
2114
|
+
if (tags.length === 0) {
|
|
2115
|
+
console.log(chalk4.dim("No labels found."));
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
console.log();
|
|
2119
|
+
for (const tag of tags) {
|
|
2120
|
+
const scope = tag.workspaceId === null ? chalk4.cyan("global") : chalk4.magenta("workspace");
|
|
2121
|
+
const category = tag.category === null ? chalk4.dim("(label)") : chalk4.dim(`(${tag.category})`);
|
|
2122
|
+
console.log(
|
|
2123
|
+
` ${chalk4.bold(tag.slug)} ${tag.name} ${chalk4.gray(tag.color)} ${scope} ${category}`
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
console.log();
|
|
2127
|
+
}
|
|
2128
|
+
function outputTagPretty(tag, prefix) {
|
|
2129
|
+
if (prefix) console.log(prefix);
|
|
2130
|
+
console.log(` ${chalk4.bold(tag.slug)} ${tag.name} ${chalk4.gray(tag.color)}`);
|
|
2131
|
+
}
|
|
2132
|
+
async function resolveLabelIds(client, workspaceId, values) {
|
|
2133
|
+
if (values.length === 0) return [];
|
|
2134
|
+
const bySlug = values.filter((v) => !looksLikeCuid(v));
|
|
2135
|
+
const byId = values.filter((v) => looksLikeCuid(v));
|
|
2136
|
+
if (bySlug.length === 0) return byId;
|
|
2137
|
+
const list = await client.labels.list({ workspaceId });
|
|
2138
|
+
const slugMap = new Map(list.allTags.map((t) => [t.slug, t.id]));
|
|
2139
|
+
const resolvedFromSlugs = [];
|
|
2140
|
+
const missing = [];
|
|
2141
|
+
for (const slug of bySlug) {
|
|
2142
|
+
const id = slugMap.get(slug);
|
|
2143
|
+
if (id === void 0) missing.push(slug);
|
|
2144
|
+
else resolvedFromSlugs.push(id);
|
|
2145
|
+
}
|
|
2146
|
+
if (missing.length > 0) {
|
|
2147
|
+
throw new Error(
|
|
2148
|
+
`Unknown label slug(s): ${missing.join(", ")}. Run \`exponential labels list --workspace <slug>\` to see available labels.`
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
return [...byId, ...resolvedFromSlugs];
|
|
2152
|
+
}
|
|
2153
|
+
function createLabelsCommand() {
|
|
2154
|
+
const labels = new Command8("labels").description(
|
|
2155
|
+
"Manage labels (orthogonal tags applied to actions, tickets, features, and epics)."
|
|
2156
|
+
);
|
|
2157
|
+
labels.command("list").description("List labels visible in a workspace").option("--workspace <slug|id>", "Workspace slug or CUID").option(
|
|
2158
|
+
"--category <category>",
|
|
2159
|
+
'Filter by category. Use "label" for general labels, "null" for category-less labels, or omit to list all.'
|
|
2160
|
+
).action(
|
|
2161
|
+
async (options, cmd) => {
|
|
2162
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2163
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
2164
|
+
try {
|
|
2165
|
+
const client = getClient();
|
|
2166
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
2167
|
+
const category = options.category === void 0 ? void 0 : options.category === "null" ? null : options.category;
|
|
2168
|
+
const result = await client.labels.list({ workspaceId, category });
|
|
2169
|
+
if (useJson) {
|
|
2170
|
+
console.log(JSON.stringify(result.allTags, null, 2));
|
|
2171
|
+
} else {
|
|
2172
|
+
outputTagsPretty(result.allTags);
|
|
2173
|
+
}
|
|
2174
|
+
} catch (error) {
|
|
2175
|
+
handleError(error, useJson);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
);
|
|
2179
|
+
labels.command("create").description("Create a workspace-scoped label").requiredOption("-n, --name <name>", "Label name").requiredOption("--color <token>", "Color token (e.g. avatar-green, brand-primary)").option("-d, --description <text>", "Description").option("--category <category>", 'Category. Omit (or pass "null") to create a general label.').option("--workspace <slug|id>", "Workspace slug or CUID").action(
|
|
2180
|
+
async (options, cmd) => {
|
|
2181
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2182
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
2183
|
+
try {
|
|
2184
|
+
const client = getClient();
|
|
2185
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
2186
|
+
if (workspaceId === void 0) {
|
|
2187
|
+
throw new Error(
|
|
2188
|
+
"Workspace is required. Pass --workspace <slug|id> or set a default with `exponential workspaces set-default`."
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
const category = options.category === void 0 ? void 0 : options.category === "null" ? null : options.category;
|
|
2192
|
+
const tag = await client.labels.create({
|
|
2193
|
+
workspaceId,
|
|
2194
|
+
name: options.name,
|
|
2195
|
+
color: options.color,
|
|
2196
|
+
description: options.description,
|
|
2197
|
+
category
|
|
2198
|
+
});
|
|
2199
|
+
if (useJson) {
|
|
2200
|
+
console.log(JSON.stringify(tag, null, 2));
|
|
2201
|
+
} else {
|
|
2202
|
+
outputTagPretty(tag, chalk4.green("\n\u2713 Label created"));
|
|
2203
|
+
}
|
|
2204
|
+
} catch (error) {
|
|
2205
|
+
handleError(error, useJson);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
);
|
|
2209
|
+
labels.command("seed-triage").description("Idempotently seed the five canonical triage labels into a workspace.").option("--workspace <slug|id>", "Workspace slug or CUID").action(
|
|
2210
|
+
async (options, cmd) => {
|
|
2211
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2212
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
2213
|
+
try {
|
|
2214
|
+
const client = getClient();
|
|
2215
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
2216
|
+
if (workspaceId === void 0) {
|
|
2217
|
+
throw new Error(
|
|
2218
|
+
"Workspace is required. Pass --workspace <slug|id> or set a default with `exponential workspaces set-default`."
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
const existing = await client.labels.list({ workspaceId });
|
|
2222
|
+
const existingSlugs = new Set(existing.allTags.map((t) => t.slug));
|
|
2223
|
+
const results = [];
|
|
2224
|
+
for (const def of TRIAGE_LABELS) {
|
|
2225
|
+
if (existingSlugs.has(def.slug)) {
|
|
2226
|
+
results.push({ slug: def.slug, status: "existed" });
|
|
2227
|
+
continue;
|
|
2228
|
+
}
|
|
2229
|
+
const tag = await client.labels.create({
|
|
2230
|
+
workspaceId,
|
|
2231
|
+
name: def.name,
|
|
2232
|
+
color: def.color
|
|
2233
|
+
});
|
|
2234
|
+
results.push({ slug: def.slug, status: "created", tag });
|
|
2235
|
+
}
|
|
2236
|
+
if (useJson) {
|
|
2237
|
+
console.log(JSON.stringify(results, null, 2));
|
|
2238
|
+
} else {
|
|
2239
|
+
console.log();
|
|
2240
|
+
console.log(chalk4.bold("Triage labels:"));
|
|
2241
|
+
for (const r of results) {
|
|
2242
|
+
const tag = chalk4.bold(r.slug);
|
|
2243
|
+
const status = r.status === "created" ? chalk4.green("created") : chalk4.dim("existed");
|
|
2244
|
+
console.log(` ${tag} ${status}`);
|
|
2245
|
+
}
|
|
2246
|
+
console.log();
|
|
2247
|
+
}
|
|
2248
|
+
} catch (error) {
|
|
2249
|
+
handleError(error, useJson);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
);
|
|
2253
|
+
return labels;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
1885
2256
|
// src/commands/products.ts
|
|
1886
|
-
import { Command as
|
|
2257
|
+
import { Command as Command9 } from "commander";
|
|
1887
2258
|
function createProductsCommand() {
|
|
1888
|
-
const products = new
|
|
2259
|
+
const products = new Command9("products").description(
|
|
1889
2260
|
"Manage products. A workspace contains products; a product holds features and tickets."
|
|
1890
2261
|
);
|
|
1891
2262
|
products.command("list").description("List products in a workspace").option("--workspace <slug|id>", "Workspace slug or CUID (defaults to configured workspace)").action(async (options, cmd) => {
|
|
@@ -1948,9 +2319,9 @@ function createProductsCommand() {
|
|
|
1948
2319
|
}
|
|
1949
2320
|
|
|
1950
2321
|
// src/commands/projects.ts
|
|
1951
|
-
import { Command as
|
|
2322
|
+
import { Command as Command10 } from "commander";
|
|
1952
2323
|
function createProjectsCommand() {
|
|
1953
|
-
const projects = new
|
|
2324
|
+
const projects = new Command10("projects").description("Manage projects");
|
|
1954
2325
|
projects.command("list").description("List all projects").option("--workspace <id>", "Filter by workspace ID").option("--include-actions", "Include actions in output").action(async (options, cmd) => {
|
|
1955
2326
|
const globalOpts = cmd.optsWithGlobals();
|
|
1956
2327
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
@@ -1973,7 +2344,10 @@ function createProjectsCommand() {
|
|
|
1973
2344
|
}
|
|
1974
2345
|
|
|
1975
2346
|
// src/commands/tickets.ts
|
|
1976
|
-
import { Command as
|
|
2347
|
+
import { Command as Command11 } from "commander";
|
|
2348
|
+
function collectRepeatable(value, previous) {
|
|
2349
|
+
return [...previous, value];
|
|
2350
|
+
}
|
|
1977
2351
|
var TICKET_STATUSES = [
|
|
1978
2352
|
"BACKLOG",
|
|
1979
2353
|
"NEEDS_REFINEMENT",
|
|
@@ -2013,33 +2387,80 @@ function validateTicketType(value) {
|
|
|
2013
2387
|
return value;
|
|
2014
2388
|
}
|
|
2015
2389
|
function createTicketsCommand() {
|
|
2016
|
-
const tickets = new
|
|
2390
|
+
const tickets = new Command11("tickets").description(
|
|
2017
2391
|
"Manage product backlog tickets. Tickets live under a product, link to features/epics, depend on each other, and can be implemented via actions."
|
|
2018
2392
|
);
|
|
2019
|
-
tickets.command("list").description(
|
|
2393
|
+
tickets.command("list").description(
|
|
2394
|
+
"List tickets in a product, or workspace-wide when --pr or --branch is supplied"
|
|
2395
|
+
).option(
|
|
2396
|
+
"--product <slug|id>",
|
|
2397
|
+
"Product slug or CUID (required unless --pr or --branch is supplied)"
|
|
2398
|
+
).option("--workspace <slug|id>", "Workspace (required when --product is a slug)").option("--status <status>", `Filter by status: ${TICKET_STATUSES.join(", ")}`).option("--type <type>", `Filter by type: ${TICKET_TYPES.join(", ")}`).option("--feature <id>", "Filter by feature CUID").option("--epic <id>", "Filter by epic CUID").option("--assignee <id>", "Filter by assignee user ID").option(
|
|
2399
|
+
"--pr <url>",
|
|
2400
|
+
"Workspace-wide lookup by Ticket.prUrl (exact match). Makes --product optional."
|
|
2401
|
+
).option(
|
|
2402
|
+
"--branch <name>",
|
|
2403
|
+
"Workspace-wide lookup by Ticket.branchName (exact match). Makes --product optional."
|
|
2404
|
+
).option(
|
|
2405
|
+
"--label <slug-or-id>",
|
|
2406
|
+
"Filter to tickets carrying this label (repeatable; AND semantics)",
|
|
2407
|
+
collectRepeatable,
|
|
2408
|
+
[]
|
|
2409
|
+
).action(
|
|
2020
2410
|
async (options, cmd) => {
|
|
2021
2411
|
const globalOpts = cmd.optsWithGlobals();
|
|
2022
2412
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
2023
2413
|
try {
|
|
2024
2414
|
const status = validateTicketStatus(options.status);
|
|
2025
2415
|
const type = validateTicketType(options.type);
|
|
2416
|
+
const hasWorkspaceFilter = Boolean(options.pr || options.branch);
|
|
2417
|
+
if (!hasWorkspaceFilter && !options.product) {
|
|
2418
|
+
throw new Error(
|
|
2419
|
+
"--product is required unless --pr or --branch is supplied"
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2026
2422
|
const client = getClient();
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
workspaceId,
|
|
2031
|
-
|
|
2032
|
-
|
|
2423
|
+
let workspaceId;
|
|
2424
|
+
let productId;
|
|
2425
|
+
if (options.product) {
|
|
2426
|
+
workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
2427
|
+
productId = await resolveProductId(
|
|
2428
|
+
client,
|
|
2429
|
+
workspaceId,
|
|
2430
|
+
options.product
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2033
2433
|
const list = await client.tickets.list({
|
|
2034
2434
|
productId,
|
|
2035
2435
|
status,
|
|
2036
2436
|
type,
|
|
2037
2437
|
featureId: options.feature,
|
|
2038
2438
|
epicId: options.epic,
|
|
2039
|
-
assigneeId: options.assignee
|
|
2439
|
+
assigneeId: options.assignee,
|
|
2440
|
+
prUrl: options.pr,
|
|
2441
|
+
branchName: options.branch
|
|
2040
2442
|
});
|
|
2041
|
-
|
|
2042
|
-
|
|
2443
|
+
let filtered = list;
|
|
2444
|
+
if (options.label.length > 0) {
|
|
2445
|
+
const labelIds = new Set(
|
|
2446
|
+
await resolveLabelIds(client, workspaceId, options.label)
|
|
2447
|
+
);
|
|
2448
|
+
const ticketLabelSets = await Promise.all(
|
|
2449
|
+
list.map(
|
|
2450
|
+
(t) => client.labels.listForEntity({ entityType: "ticket", entityId: t.id }).then((tags) => new Set(tags.map((tag) => tag.id)))
|
|
2451
|
+
)
|
|
2452
|
+
);
|
|
2453
|
+
filtered = list.filter((_t, idx) => {
|
|
2454
|
+
const tagSet = ticketLabelSets[idx];
|
|
2455
|
+
if (tagSet === void 0) return false;
|
|
2456
|
+
for (const id of labelIds) {
|
|
2457
|
+
if (!tagSet.has(id)) return false;
|
|
2458
|
+
}
|
|
2459
|
+
return true;
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
if (useJson) outputTicketsJson(filtered);
|
|
2463
|
+
else outputTicketsPretty(filtered);
|
|
2043
2464
|
} catch (error) {
|
|
2044
2465
|
handleError(error, useJson);
|
|
2045
2466
|
}
|
|
@@ -2069,7 +2490,12 @@ function createTicketsCommand() {
|
|
|
2069
2490
|
handleError(error, useJson);
|
|
2070
2491
|
}
|
|
2071
2492
|
});
|
|
2072
|
-
tickets.command("create").description("Create a new ticket in a product").requiredOption("--product <slug|id>", "Product slug or CUID").requiredOption("-t, --title <title>", "Ticket title").option("-b, --body <text>", "Body (markdown supported)").option("--type <type>", `Ticket type: ${TICKET_TYPES.join(", ")}`).option("--status <status>", `Initial status: ${TICKET_STATUSES.join(", ")}`).option("--priority <n>", "Priority 0-4 (lower = higher)", parseInt).option("--points <n>", "Story points", parseFloat).option("--feature <id>", "Feature CUID to attach to").option("--epic <id>", "Epic CUID to attach to").option("--assignee <id>", "Assignee user ID").option("--branch <name>", "Branch name").option("--pr <url>", "Pull request URL").option("--workspace <slug|id>", "Workspace (required when --product is a slug)").
|
|
2493
|
+
tickets.command("create").description("Create a new ticket in a product").requiredOption("--product <slug|id>", "Product slug or CUID").requiredOption("-t, --title <title>", "Ticket title").option("-b, --body <text>", "Body (markdown supported)").option("--type <type>", `Ticket type: ${TICKET_TYPES.join(", ")}`).option("--status <status>", `Initial status: ${TICKET_STATUSES.join(", ")}`).option("--priority <n>", "Priority 0-4 (lower = higher)", parseInt).option("--points <n>", "Story points", parseFloat).option("--feature <id>", "Feature CUID to attach to").option("--epic <id>", "Epic CUID to attach to").option("--assignee <id>", "Assignee user ID").option("--branch <name>", "Branch name").option("--pr <url>", "Pull request URL").option("--workspace <slug|id>", "Workspace (required when --product is a slug)").option(
|
|
2494
|
+
"--label <slug-or-id>",
|
|
2495
|
+
"Label to attach (repeatable)",
|
|
2496
|
+
collectRepeatable,
|
|
2497
|
+
[]
|
|
2498
|
+
).action(
|
|
2073
2499
|
async (options, cmd) => {
|
|
2074
2500
|
const globalOpts = cmd.optsWithGlobals();
|
|
2075
2501
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
@@ -2097,6 +2523,14 @@ function createTicketsCommand() {
|
|
|
2097
2523
|
branchName: options.branch,
|
|
2098
2524
|
prUrl: options.pr
|
|
2099
2525
|
});
|
|
2526
|
+
if (options.label.length > 0) {
|
|
2527
|
+
const tagIds = await resolveLabelIds(client, workspaceId, options.label);
|
|
2528
|
+
await client.labels.setEntityTags({
|
|
2529
|
+
entityType: "ticket",
|
|
2530
|
+
entityId: ticket.id,
|
|
2531
|
+
tagIds
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2100
2534
|
if (useJson) outputTicketJson(ticket);
|
|
2101
2535
|
else {
|
|
2102
2536
|
console.log("\n\u2713 Ticket created");
|
|
@@ -2107,7 +2541,17 @@ function createTicketsCommand() {
|
|
|
2107
2541
|
}
|
|
2108
2542
|
}
|
|
2109
2543
|
);
|
|
2110
|
-
tickets.command("update").description("Update an existing ticket").requiredOption("--id <id>", "Ticket CUID").option("-t, --title <title>", "New title").option("-b, --body <text>", "New body").option("--type <type>", `Type: ${TICKET_TYPES.join(", ")}`).option("--status <status>", `Status: ${TICKET_STATUSES.join(", ")}`).option("--priority <n>", 'Priority 0-4 (or "null" to clear)').option("--points <n>", 'Story points (or "null" to clear)').option("--feature <id>", 'Feature CUID (or "null" to detach)').option("--epic <id>", 'Epic CUID (or "null" to detach)').option("--assignee <id>", 'Assignee user ID (or "null" to unassign)').option("--branch <name>", 'Branch name (or "null")').option("--pr <url>", 'PR URL (or "null")').
|
|
2544
|
+
tickets.command("update").description("Update an existing ticket").requiredOption("--id <id>", "Ticket CUID").option("-t, --title <title>", "New title").option("-b, --body <text>", "New body").option("--type <type>", `Type: ${TICKET_TYPES.join(", ")}`).option("--status <status>", `Status: ${TICKET_STATUSES.join(", ")}`).option("--priority <n>", 'Priority 0-4 (or "null" to clear)').option("--points <n>", 'Story points (or "null" to clear)').option("--feature <id>", 'Feature CUID (or "null" to detach)').option("--epic <id>", 'Epic CUID (or "null" to detach)').option("--assignee <id>", 'Assignee user ID (or "null" to unassign)').option("--branch <name>", 'Branch name (or "null")').option("--pr <url>", 'PR URL (or "null")').option(
|
|
2545
|
+
"--add-label <slug-or-id>",
|
|
2546
|
+
"Label to attach to this ticket (repeatable)",
|
|
2547
|
+
collectRepeatable,
|
|
2548
|
+
[]
|
|
2549
|
+
).option(
|
|
2550
|
+
"--remove-label <slug-or-id>",
|
|
2551
|
+
"Label to detach from this ticket (repeatable)",
|
|
2552
|
+
collectRepeatable,
|
|
2553
|
+
[]
|
|
2554
|
+
).action(
|
|
2111
2555
|
async (options, cmd) => {
|
|
2112
2556
|
const globalOpts = cmd.optsWithGlobals();
|
|
2113
2557
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
@@ -2148,6 +2592,22 @@ function createTicketsCommand() {
|
|
|
2148
2592
|
branchName: parseNullableString(options.branch),
|
|
2149
2593
|
prUrl: parseNullableString(options.pr)
|
|
2150
2594
|
});
|
|
2595
|
+
if (options.addLabel.length > 0 || options.removeLabel.length > 0) {
|
|
2596
|
+
const current = await client.labels.listForEntity({
|
|
2597
|
+
entityType: "ticket",
|
|
2598
|
+
entityId: options.id
|
|
2599
|
+
});
|
|
2600
|
+
const currentIds = new Set(current.map((t) => t.id));
|
|
2601
|
+
const addIds = await resolveLabelIds(client, void 0, options.addLabel);
|
|
2602
|
+
const removeIds = await resolveLabelIds(client, void 0, options.removeLabel);
|
|
2603
|
+
for (const id of addIds) currentIds.add(id);
|
|
2604
|
+
for (const id of removeIds) currentIds.delete(id);
|
|
2605
|
+
await client.labels.setEntityTags({
|
|
2606
|
+
entityType: "ticket",
|
|
2607
|
+
entityId: options.id,
|
|
2608
|
+
tagIds: [...currentIds]
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2151
2611
|
if (useJson) outputTicketJson(ticket);
|
|
2152
2612
|
else {
|
|
2153
2613
|
console.log("\n\u2713 Ticket updated");
|
|
@@ -2240,7 +2700,7 @@ function createTicketsCommand() {
|
|
|
2240
2700
|
handleError(error, useJson);
|
|
2241
2701
|
}
|
|
2242
2702
|
});
|
|
2243
|
-
const comment = new
|
|
2703
|
+
const comment = new Command11("comment").description("Manage comments on a ticket");
|
|
2244
2704
|
comment.command("add").description("Add a comment to a ticket").requiredOption("--id <id>", "Ticket CUID").requiredOption("-m, --message <text>", "Comment content (markdown supported)").action(async (options, cmd) => {
|
|
2245
2705
|
const globalOpts = cmd.optsWithGlobals();
|
|
2246
2706
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
@@ -2279,10 +2739,10 @@ function createTicketsCommand() {
|
|
|
2279
2739
|
}
|
|
2280
2740
|
|
|
2281
2741
|
// src/commands/workspaces.ts
|
|
2282
|
-
import { Command as
|
|
2283
|
-
import
|
|
2742
|
+
import { Command as Command12 } from "commander";
|
|
2743
|
+
import chalk5 from "chalk";
|
|
2284
2744
|
function createWorkspacesCommand() {
|
|
2285
|
-
const workspaces = new
|
|
2745
|
+
const workspaces = new Command12("workspaces").description("Manage workspaces");
|
|
2286
2746
|
workspaces.command("list").description("List all workspaces").action(async (_options, cmd) => {
|
|
2287
2747
|
const globalOpts = cmd.optsWithGlobals();
|
|
2288
2748
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
@@ -2295,7 +2755,7 @@ function createWorkspacesCommand() {
|
|
|
2295
2755
|
const config = getConfig();
|
|
2296
2756
|
outputWorkspacesPretty(workspaces2);
|
|
2297
2757
|
if (config.defaultWorkspaceSlug) {
|
|
2298
|
-
console.log(
|
|
2758
|
+
console.log(chalk5.gray(`Default workspace: ${config.defaultWorkspaceSlug}`));
|
|
2299
2759
|
}
|
|
2300
2760
|
}
|
|
2301
2761
|
} catch (error) {
|
|
@@ -2308,10 +2768,10 @@ function createWorkspacesCommand() {
|
|
|
2308
2768
|
const workspaces2 = await client.workspaces.list();
|
|
2309
2769
|
const workspace = workspaces2.find((w) => w.slug === slug);
|
|
2310
2770
|
if (!workspace) {
|
|
2311
|
-
console.error(
|
|
2312
|
-
console.log(
|
|
2771
|
+
console.error(chalk5.red(`Workspace with slug "${slug}" not found.`));
|
|
2772
|
+
console.log(chalk5.gray("Available workspaces:"));
|
|
2313
2773
|
workspaces2.forEach((w) => {
|
|
2314
|
-
console.log(
|
|
2774
|
+
console.log(chalk5.gray(` - ${w.slug} (${w.name})`));
|
|
2315
2775
|
});
|
|
2316
2776
|
process.exit(1);
|
|
2317
2777
|
}
|
|
@@ -2319,7 +2779,7 @@ function createWorkspacesCommand() {
|
|
|
2319
2779
|
defaultWorkspaceId: workspace.id,
|
|
2320
2780
|
defaultWorkspaceSlug: workspace.slug
|
|
2321
2781
|
});
|
|
2322
|
-
console.log(
|
|
2782
|
+
console.log(chalk5.green(`Default workspace set to: ${workspace.name} (${workspace.slug})`));
|
|
2323
2783
|
} catch (error) {
|
|
2324
2784
|
handleError(error);
|
|
2325
2785
|
}
|
|
@@ -2328,7 +2788,7 @@ function createWorkspacesCommand() {
|
|
|
2328
2788
|
}
|
|
2329
2789
|
|
|
2330
2790
|
// src/index.ts
|
|
2331
|
-
var program = new
|
|
2791
|
+
var program = new Command13();
|
|
2332
2792
|
program.name("exponential").description(
|
|
2333
2793
|
[
|
|
2334
2794
|
"CLI to interact with Exponential productivity app.",
|
|
@@ -2344,6 +2804,7 @@ program.addCommand(createContactsCommand());
|
|
|
2344
2804
|
program.addCommand(createDealsCommand());
|
|
2345
2805
|
program.addCommand(createEpicsCommand());
|
|
2346
2806
|
program.addCommand(createFeaturesCommand());
|
|
2807
|
+
program.addCommand(createLabelsCommand());
|
|
2347
2808
|
program.addCommand(createProductsCommand());
|
|
2348
2809
|
program.addCommand(createProjectsCommand());
|
|
2349
2810
|
program.addCommand(createTicketsCommand());
|