exponential-cli 1.2.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 +259 -22
- 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,11 +2092,12 @@ function createFeaturesCommand() {
|
|
|
1879
2092
|
}
|
|
1880
2093
|
}
|
|
1881
2094
|
);
|
|
2095
|
+
features.addCommand(createStoriesCommand());
|
|
1882
2096
|
return features;
|
|
1883
2097
|
}
|
|
1884
2098
|
|
|
1885
2099
|
// src/commands/labels.ts
|
|
1886
|
-
import { Command as
|
|
2100
|
+
import { Command as Command8 } from "commander";
|
|
1887
2101
|
import chalk4 from "chalk";
|
|
1888
2102
|
var TRIAGE_LABELS = [
|
|
1889
2103
|
{ slug: "needs-triage", name: "needs-triage", color: "avatar-orange" },
|
|
@@ -1937,7 +2151,7 @@ async function resolveLabelIds(client, workspaceId, values) {
|
|
|
1937
2151
|
return [...byId, ...resolvedFromSlugs];
|
|
1938
2152
|
}
|
|
1939
2153
|
function createLabelsCommand() {
|
|
1940
|
-
const labels = new
|
|
2154
|
+
const labels = new Command8("labels").description(
|
|
1941
2155
|
"Manage labels (orthogonal tags applied to actions, tickets, features, and epics)."
|
|
1942
2156
|
);
|
|
1943
2157
|
labels.command("list").description("List labels visible in a workspace").option("--workspace <slug|id>", "Workspace slug or CUID").option(
|
|
@@ -2040,9 +2254,9 @@ function createLabelsCommand() {
|
|
|
2040
2254
|
}
|
|
2041
2255
|
|
|
2042
2256
|
// src/commands/products.ts
|
|
2043
|
-
import { Command as
|
|
2257
|
+
import { Command as Command9 } from "commander";
|
|
2044
2258
|
function createProductsCommand() {
|
|
2045
|
-
const products = new
|
|
2259
|
+
const products = new Command9("products").description(
|
|
2046
2260
|
"Manage products. A workspace contains products; a product holds features and tickets."
|
|
2047
2261
|
);
|
|
2048
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) => {
|
|
@@ -2105,9 +2319,9 @@ function createProductsCommand() {
|
|
|
2105
2319
|
}
|
|
2106
2320
|
|
|
2107
2321
|
// src/commands/projects.ts
|
|
2108
|
-
import { Command as
|
|
2322
|
+
import { Command as Command10 } from "commander";
|
|
2109
2323
|
function createProjectsCommand() {
|
|
2110
|
-
const projects = new
|
|
2324
|
+
const projects = new Command10("projects").description("Manage projects");
|
|
2111
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) => {
|
|
2112
2326
|
const globalOpts = cmd.optsWithGlobals();
|
|
2113
2327
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
@@ -2130,7 +2344,7 @@ function createProjectsCommand() {
|
|
|
2130
2344
|
}
|
|
2131
2345
|
|
|
2132
2346
|
// src/commands/tickets.ts
|
|
2133
|
-
import { Command as
|
|
2347
|
+
import { Command as Command11 } from "commander";
|
|
2134
2348
|
function collectRepeatable(value, previous) {
|
|
2135
2349
|
return [...previous, value];
|
|
2136
2350
|
}
|
|
@@ -2173,10 +2387,21 @@ function validateTicketType(value) {
|
|
|
2173
2387
|
return value;
|
|
2174
2388
|
}
|
|
2175
2389
|
function createTicketsCommand() {
|
|
2176
|
-
const tickets = new
|
|
2390
|
+
const tickets = new Command11("tickets").description(
|
|
2177
2391
|
"Manage product backlog tickets. Tickets live under a product, link to features/epics, depend on each other, and can be implemented via actions."
|
|
2178
2392
|
);
|
|
2179
|
-
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(
|
|
2180
2405
|
"--label <slug-or-id>",
|
|
2181
2406
|
"Filter to tickets carrying this label (repeatable; AND semantics)",
|
|
2182
2407
|
collectRepeatable,
|
|
@@ -2188,20 +2413,32 @@ function createTicketsCommand() {
|
|
|
2188
2413
|
try {
|
|
2189
2414
|
const status = validateTicketStatus(options.status);
|
|
2190
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
|
+
}
|
|
2191
2422
|
const client = getClient();
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
workspaceId,
|
|
2196
|
-
|
|
2197
|
-
|
|
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
|
+
}
|
|
2198
2433
|
const list = await client.tickets.list({
|
|
2199
2434
|
productId,
|
|
2200
2435
|
status,
|
|
2201
2436
|
type,
|
|
2202
2437
|
featureId: options.feature,
|
|
2203
2438
|
epicId: options.epic,
|
|
2204
|
-
assigneeId: options.assignee
|
|
2439
|
+
assigneeId: options.assignee,
|
|
2440
|
+
prUrl: options.pr,
|
|
2441
|
+
branchName: options.branch
|
|
2205
2442
|
});
|
|
2206
2443
|
let filtered = list;
|
|
2207
2444
|
if (options.label.length > 0) {
|
|
@@ -2463,7 +2700,7 @@ function createTicketsCommand() {
|
|
|
2463
2700
|
handleError(error, useJson);
|
|
2464
2701
|
}
|
|
2465
2702
|
});
|
|
2466
|
-
const comment = new
|
|
2703
|
+
const comment = new Command11("comment").description("Manage comments on a ticket");
|
|
2467
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) => {
|
|
2468
2705
|
const globalOpts = cmd.optsWithGlobals();
|
|
2469
2706
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
@@ -2502,10 +2739,10 @@ function createTicketsCommand() {
|
|
|
2502
2739
|
}
|
|
2503
2740
|
|
|
2504
2741
|
// src/commands/workspaces.ts
|
|
2505
|
-
import { Command as
|
|
2742
|
+
import { Command as Command12 } from "commander";
|
|
2506
2743
|
import chalk5 from "chalk";
|
|
2507
2744
|
function createWorkspacesCommand() {
|
|
2508
|
-
const workspaces = new
|
|
2745
|
+
const workspaces = new Command12("workspaces").description("Manage workspaces");
|
|
2509
2746
|
workspaces.command("list").description("List all workspaces").action(async (_options, cmd) => {
|
|
2510
2747
|
const globalOpts = cmd.optsWithGlobals();
|
|
2511
2748
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
@@ -2551,7 +2788,7 @@ function createWorkspacesCommand() {
|
|
|
2551
2788
|
}
|
|
2552
2789
|
|
|
2553
2790
|
// src/index.ts
|
|
2554
|
-
var program = new
|
|
2791
|
+
var program = new Command13();
|
|
2555
2792
|
program.name("exponential").description(
|
|
2556
2793
|
[
|
|
2557
2794
|
"CLI to interact with Exponential productivity app.",
|