@quire-io/quire-cli 0.1.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.
Files changed (94) hide show
  1. package/CHANGES.md +33 -0
  2. package/LICENSE +15 -0
  3. package/README.md +69 -0
  4. package/dist/cli.js +209 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/chat.js +200 -0
  7. package/dist/commands/chat.js.map +1 -0
  8. package/dist/commands/colors.js +34 -0
  9. package/dist/commands/colors.js.map +1 -0
  10. package/dist/commands/comment.js +128 -0
  11. package/dist/commands/comment.js.map +1 -0
  12. package/dist/commands/doc.js +139 -0
  13. package/dist/commands/doc.js.map +1 -0
  14. package/dist/commands/insight.js +234 -0
  15. package/dist/commands/insight.js.map +1 -0
  16. package/dist/commands/login.js +78 -0
  17. package/dist/commands/login.js.map +1 -0
  18. package/dist/commands/logout.js +25 -0
  19. package/dist/commands/logout.js.map +1 -0
  20. package/dist/commands/mine.js +61 -0
  21. package/dist/commands/mine.js.map +1 -0
  22. package/dist/commands/notify.js +25 -0
  23. package/dist/commands/notify.js.map +1 -0
  24. package/dist/commands/org.js +99 -0
  25. package/dist/commands/org.js.map +1 -0
  26. package/dist/commands/partner.js +36 -0
  27. package/dist/commands/partner.js.map +1 -0
  28. package/dist/commands/project.js +346 -0
  29. package/dist/commands/project.js.map +1 -0
  30. package/dist/commands/resolve.js +61 -0
  31. package/dist/commands/resolve.js.map +1 -0
  32. package/dist/commands/status.js +130 -0
  33. package/dist/commands/status.js.map +1 -0
  34. package/dist/commands/sublist.js +143 -0
  35. package/dist/commands/sublist.js.map +1 -0
  36. package/dist/commands/tag.js +108 -0
  37. package/dist/commands/tag.js.map +1 -0
  38. package/dist/commands/task.js +767 -0
  39. package/dist/commands/task.js.map +1 -0
  40. package/dist/commands/undo.js +64 -0
  41. package/dist/commands/undo.js.map +1 -0
  42. package/dist/commands/user.js +24 -0
  43. package/dist/commands/user.js.map +1 -0
  44. package/dist/commands/whoami.js +34 -0
  45. package/dist/commands/whoami.js.map +1 -0
  46. package/dist/config.js +37 -0
  47. package/dist/config.js.map +1 -0
  48. package/dist/credentials.js +28 -0
  49. package/dist/credentials.js.map +1 -0
  50. package/dist/errors.js +56 -0
  51. package/dist/errors.js.map +1 -0
  52. package/dist/log.js +37 -0
  53. package/dist/log.js.map +1 -0
  54. package/dist/oauth/config.js +22 -0
  55. package/dist/oauth/config.js.map +1 -0
  56. package/dist/oauth/loopback.js +101 -0
  57. package/dist/oauth/loopback.js.map +1 -0
  58. package/dist/oauth/pkce.js +16 -0
  59. package/dist/oauth/pkce.js.map +1 -0
  60. package/dist/options.js +2 -0
  61. package/dist/options.js.map +1 -0
  62. package/dist/output/colors.js +77 -0
  63. package/dist/output/colors.js.map +1 -0
  64. package/dist/output/columns.js +22 -0
  65. package/dist/output/columns.js.map +1 -0
  66. package/dist/output/keyvalue.js +18 -0
  67. package/dist/output/keyvalue.js.map +1 -0
  68. package/dist/output/render.js +30 -0
  69. package/dist/output/render.js.map +1 -0
  70. package/dist/output/table.js +35 -0
  71. package/dist/output/table.js.map +1 -0
  72. package/dist/quire-client.js +74 -0
  73. package/dist/quire-client.js.map +1 -0
  74. package/dist/util/attach-input.js +65 -0
  75. package/dist/util/attach-input.js.map +1 -0
  76. package/dist/util/bulk-input.js +80 -0
  77. package/dist/util/bulk-input.js.map +1 -0
  78. package/dist/util/confirm.js +31 -0
  79. package/dist/util/confirm.js.map +1 -0
  80. package/dist/util/field-flags.js +52 -0
  81. package/dist/util/field-flags.js.map +1 -0
  82. package/dist/util/open-url.js +39 -0
  83. package/dist/util/open-url.js.map +1 -0
  84. package/dist/util/recurrence.js +53 -0
  85. package/dist/util/recurrence.js.map +1 -0
  86. package/dist/util/retry-after.js +56 -0
  87. package/dist/util/retry-after.js.map +1 -0
  88. package/dist/util/task-id.js +34 -0
  89. package/dist/util/task-id.js.map +1 -0
  90. package/dist/util/text-input.js +26 -0
  91. package/dist/util/text-input.js.map +1 -0
  92. package/dist/version.js +19 -0
  93. package/dist/version.js.map +1 -0
  94. package/package.json +62 -0
@@ -0,0 +1,25 @@
1
+ import { resolveConfigPaths } from "../config.js";
2
+ import { deleteCredentials, readCredentials } from "../credentials.js";
3
+ import { createLogger } from "../log.js";
4
+ export function registerLogoutCommand(program) {
5
+ program
6
+ .command("logout")
7
+ .description("Delete the local credentials file. Note: this does not revoke the token server-side — to fully revoke, remove the app at https://quire.io/apps.")
8
+ .action(() => {
9
+ const root = program.opts();
10
+ const log = createLogger({ verbose: root.verbose === true, color: root.colorMode });
11
+ const stored = readCredentials(root.profile);
12
+ const { credentialsPath } = resolveConfigPaths(root.profile);
13
+ if (!stored) {
14
+ if (root.quiet !== true) {
15
+ log.info(`No credentials found at ${credentialsPath} — already logged out.`);
16
+ }
17
+ return;
18
+ }
19
+ deleteCredentials(root.profile);
20
+ if (root.quiet !== true) {
21
+ log.info(`Logged out — removed ${credentialsPath}. To revoke the token on the Quire server, remove the app at https://quire.io/apps.`);
22
+ }
23
+ });
24
+ }
25
+ //# sourceMappingURL=logout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logout.js","sourceRoot":"","sources":["../../src/commands/logout.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AASzC,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,OAAO;SACJ,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CACV,iJAAiJ,CAClJ;SACA,MAAM,CAAC,GAAG,EAAE;QACX,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAc,CAAC;QACxC,MAAM,GAAG,GAAG,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,KAAK,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QAEpF,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC7C,MAAM,EAAE,eAAe,EAAE,GAAG,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAE7D,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;gBACxB,GAAG,CAAC,IAAI,CAAC,2BAA2B,eAAe,wBAAwB,CAAC,CAAC;YAC/E,CAAC;YACD,OAAO;QACT,CAAC;QAED,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAChC,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;YACxB,GAAG,CAAC,IAAI,CACN,wBAAwB,eAAe,qFAAqF,CAC7H,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,61 @@
1
+ import { ValidationError } from "../errors.js";
2
+ import { createLogger } from "../log.js";
3
+ import { TASK_LIST_COLUMNS } from "../output/columns.js";
4
+ import { renderList } from "../output/render.js";
5
+ import { createQuireClient } from "../quire-client.js";
6
+ function parseLimit(input) {
7
+ if (input === undefined)
8
+ return undefined;
9
+ if (input === "no")
10
+ return "no";
11
+ const n = Number.parseInt(input, 10);
12
+ if (!Number.isInteger(n) || n <= 0) {
13
+ throw new ValidationError(`--limit must be a positive integer or 'no'; got "${input}"`);
14
+ }
15
+ return n;
16
+ }
17
+ export function registerMineCommand(program) {
18
+ program
19
+ .command("mine")
20
+ .description("List tasks assigned to me. Scope with --project, --org, or --all-orgs.")
21
+ .option("--project <id>", "Restrict to one project.")
22
+ .option("--org <id>", "Restrict to one organization.")
23
+ .option("--all-orgs", "Fan out across every organization you belong to.")
24
+ .option("--limit <n>", "Page size.")
25
+ .action(async (cmdOpts) => {
26
+ const root = program.opts();
27
+ const log = createLogger({ verbose: root.verbose === true, color: root.colorMode });
28
+ const client = createQuireClient({ profile: root.profile });
29
+ const params = {
30
+ mine: true,
31
+ ...(cmdOpts.limit ? { limit: parseLimit(cmdOpts.limit) } : {}),
32
+ };
33
+ let tasks;
34
+ if (cmdOpts.project) {
35
+ const oid = await client.resolveProjectOid(cmdOpts.project);
36
+ tasks = await client.searchTasks(oid, params);
37
+ }
38
+ else if (cmdOpts.org) {
39
+ const oid = await client.resolveOrgOid(cmdOpts.org);
40
+ tasks = await client.searchTasksInOrganization(oid, params);
41
+ }
42
+ else if (cmdOpts.allOrgs === true) {
43
+ const orgs = await client.listOrganizations();
44
+ if (orgs.length === 0) {
45
+ tasks = [];
46
+ }
47
+ else {
48
+ if (orgs.length > 1) {
49
+ log.info(`Searching ${orgs.length} organizations…`);
50
+ }
51
+ const lists = await Promise.all(orgs.map((o) => client.searchTasksInOrganization(o.oid, params)));
52
+ tasks = lists.flat();
53
+ }
54
+ }
55
+ else {
56
+ throw new ValidationError("`quire mine` requires one of --project, --org, or --all-orgs.");
57
+ }
58
+ renderList(tasks, root, { columns: TASK_LIST_COLUMNS, toId: (t) => t.oid });
59
+ });
60
+ }
61
+ //# sourceMappingURL=mine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mine.js","sourceRoot":"","sources":["../../src/commands/mine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import { createQuireClient } from "../quire-client.js";
2
+ import { resolveTextInput } from "../util/text-input.js";
3
+ export function registerNotifyCommand(program) {
4
+ program
5
+ .command("notify")
6
+ .description("Send an in-app notification to yourself. Requires the `share` OAuth scope.")
7
+ .requiredOption("--message <text>", "Notification text ('-' = stdin, '@file' = file)")
8
+ .option("--url <url>", "Optional link the notification opens when clicked")
9
+ .action(async (cmdOpts) => {
10
+ const root = program.opts();
11
+ const client = createQuireClient({ profile: root.profile });
12
+ const message = await resolveTextInput(cmdOpts.message);
13
+ await client.sendNotification({
14
+ message,
15
+ ...(cmdOpts.url !== undefined ? { url: cmdOpts.url } : {}),
16
+ });
17
+ if (root.json === true) {
18
+ process.stdout.write(`${JSON.stringify({ sent: true })}\n`);
19
+ }
20
+ else if (root.quiet !== true) {
21
+ process.stderr.write("Notification sent.\n");
22
+ }
23
+ });
24
+ }
25
+ //# sourceMappingURL=notify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notify.js","sourceRoot":"","sources":["../../src/commands/notify.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,OAAO;SACJ,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,4EAA4E,CAAC;SACzF,cAAc,CAAC,kBAAkB,EAAE,iDAAiD,CAAC;SACrF,MAAM,CAAC,aAAa,EAAE,mDAAmD,CAAC;SAC1E,MAAM,CAAC,KAAK,EAAE,OAA0C,EAAE,EAAE;QAC3D,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAc,CAAC;QACxC,MAAM,MAAM,GAAG,iBAAiB,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QAC5D,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACxD,MAAM,MAAM,CAAC,gBAAgB,CAAC;YAC5B,OAAO;YACP,GAAG,CAAC,OAAO,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC3D,CAAC,CAAC;QACH,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;YACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC;QAC9D,CAAC;aAAM,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;YAC/B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,99 @@
1
+ import { ValidationError } from "../errors.js";
2
+ import { renderList, renderObject } from "../output/render.js";
3
+ import { createQuireClient } from "../quire-client.js";
4
+ import { resolveTextInput } from "../util/text-input.js";
5
+ const ORG_GET_FIELDS = [
6
+ { label: "Name", get: (o) => o.nameText ?? o.name },
7
+ { label: "ID", get: (o) => o.id },
8
+ { label: "OID", get: (o) => o.oid },
9
+ { label: "Email", get: (o) => o.email },
10
+ { label: "Website", get: (o) => o.website },
11
+ { label: "Description", get: (o) => o.descriptionText },
12
+ { label: "URL", get: (o) => o.url },
13
+ { label: "Plan", get: (o) => o.subscription?.plan },
14
+ { label: "Created at", get: (o) => o.createdAt },
15
+ ];
16
+ const append = (val, prev) => [...(prev ?? []), val];
17
+ export function registerOrgCommand(program) {
18
+ const org = program.command("org").description("Quire organizations.");
19
+ org
20
+ .command("list")
21
+ .description("List organizations you belong to.")
22
+ .action(async () => {
23
+ const root = program.opts();
24
+ const client = createQuireClient({ profile: root.profile });
25
+ const orgs = await client.listOrganizations();
26
+ renderList(orgs, root, {
27
+ columns: [
28
+ { header: "ID", get: (o) => o.id },
29
+ { header: "NAME", get: (o) => o.nameText ?? o.name },
30
+ { header: "OID", get: (o) => o.oid },
31
+ ],
32
+ toId: (o) => o.oid,
33
+ });
34
+ });
35
+ org
36
+ .command("limit <id>")
37
+ .description("Show API rate-limit usage for an organization.")
38
+ .action(async (id) => {
39
+ const root = program.opts();
40
+ const client = createQuireClient({ profile: root.profile });
41
+ const oid = await client.resolveOrgOid(id);
42
+ const limit = await client.getRateLimit(oid);
43
+ const formatBucket = (b) => {
44
+ // Quire's `expiresAt` is documented in epoch ms; assume `reset` follows
45
+ // the same convention. If a Date round-trips to year 1970, fall back to
46
+ // treating the number as seconds.
47
+ const isoMs = new Date(b.reset).toISOString();
48
+ const iso = isoMs.startsWith("1970") ? new Date(b.reset * 1000).toISOString() : isoMs;
49
+ return `${b.used} / ${b.limit} used (${b.remaining} remaining, resets ${iso})`;
50
+ };
51
+ renderObject(limit, root, {
52
+ fields: [
53
+ { label: "Organization", get: (l) => l.organization },
54
+ { label: "Plan", get: (l) => l.plan },
55
+ { label: "Per minute", get: (l) => formatBucket(l.minute) },
56
+ { label: "Per hour", get: (l) => formatBucket(l.hour) },
57
+ ],
58
+ toId: (l) => l.organization,
59
+ });
60
+ });
61
+ org
62
+ .command("get <id>")
63
+ .description("Show details for one organization. <id> = OID, slug, or full URL.")
64
+ .action(async (id) => {
65
+ const root = program.opts();
66
+ const client = createQuireClient({ profile: root.profile });
67
+ const oid = await client.resolveOrgOid(id);
68
+ const o = await client.getOrganization(oid);
69
+ renderObject(o, root, { fields: ORG_GET_FIELDS, toId: (o) => o.oid });
70
+ });
71
+ org
72
+ .command("update <id>")
73
+ .description("Update organization metadata.")
74
+ .option("--name <name>", "New name")
75
+ .option("--description <text>", "New description ('-' = stdin, '@file' = file)")
76
+ .option("--add-follower <user>", "Add follower; repeat for multiple", append, [])
77
+ .option("--remove-follower <user>", "Remove follower; repeat for multiple", append, [])
78
+ .action(async (id, cmdOpts) => {
79
+ const root = program.opts();
80
+ const client = createQuireClient({ profile: root.profile });
81
+ const oid = await client.resolveOrgOid(id);
82
+ const description = cmdOpts.description !== undefined ? await resolveTextInput(cmdOpts.description) : undefined;
83
+ const body = {};
84
+ if (cmdOpts.name !== undefined)
85
+ body.name = cmdOpts.name;
86
+ if (description !== undefined)
87
+ body.description = description;
88
+ if ((cmdOpts.addFollower?.length ?? 0) > 0)
89
+ body.addFollowers = cmdOpts.addFollower;
90
+ if ((cmdOpts.removeFollower?.length ?? 0) > 0)
91
+ body.removeFollowers = cmdOpts.removeFollower;
92
+ if (Object.keys(body).length === 0) {
93
+ throw new ValidationError("`org update` requires at least one of --name / --description / --add-follower / --remove-follower.");
94
+ }
95
+ const o = await client.updateOrganization(oid, body);
96
+ renderObject(o, root, { fields: ORG_GET_FIELDS, toId: (o) => o.oid });
97
+ });
98
+ }
99
+ //# sourceMappingURL=org.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"org.js","sourceRoot":"","sources":["../../src/commands/org.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { renderList, renderObject } from "../output/render.js";
2
+ import { createQuireClient } from "../quire-client.js";
3
+ const PARTNER_FIELDS = [
4
+ { label: "Name", get: (p) => p.name },
5
+ { label: "OID", get: (p) => p.oid },
6
+ { label: "Color", get: (p) => p.color === undefined ? undefined : String(p.color) },
7
+ ];
8
+ export function registerPartnerCommand(program) {
9
+ const partner = program.command("partner").description("Quire partner organizations (external teams).");
10
+ partner
11
+ .command("list <project>")
12
+ .description("List partner organizations on a project. <project> = OID, slug, or URL.")
13
+ .action(async (project) => {
14
+ const root = program.opts();
15
+ const client = createQuireClient({ profile: root.profile });
16
+ const projectOid = await client.resolveProjectOid(project);
17
+ const partners = await client.listPartners(projectOid);
18
+ renderList(partners, root, {
19
+ columns: [
20
+ { header: "NAME", get: (p) => p.name },
21
+ { header: "OID", get: (p) => p.oid },
22
+ ],
23
+ toId: (p) => p.oid,
24
+ });
25
+ });
26
+ partner
27
+ .command("get <oid>")
28
+ .description("Show one partner organization.")
29
+ .action(async (oid) => {
30
+ const root = program.opts();
31
+ const client = createQuireClient({ profile: root.profile });
32
+ const p = await client.getPartner(oid);
33
+ renderObject(p, root, { fields: PARTNER_FIELDS, toId: (p) => p.oid });
34
+ });
35
+ }
36
+ //# sourceMappingURL=partner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"partner.js","sourceRoot":"","sources":["../../src/commands/partner.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,cAAc,GAAG;IACrB,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAmB,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE;IACvD,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAkB,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE;IACpD,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAqB,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE;CACxG,CAAC;AAEF,MAAM,UAAU,sBAAsB,CAAC,OAAgB;IACrD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,WAAW,CAAC,+CAA+C,CAAC,CAAC;IAExG,OAAO;SACJ,OAAO,CAAC,gBAAgB,CAAC;SACzB,WAAW,CAAC,yEAAyE,CAAC;SACtF,MAAM,CAAC,KAAK,EAAE,OAAe,EAAE,EAAE;QAChC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAc,CAAC;QACxC,MAAM,MAAM,GAAG,iBAAiB,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QAC5D,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAC3D,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;QACvD,UAAU,CAAC,QAAQ,EAAE,IAAI,EAAE;YACzB,OAAO,EAAE;gBACP,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE;gBACtC,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE;aACrC;YACD,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG;SACnB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,WAAW,CAAC;SACpB,WAAW,CAAC,gCAAgC,CAAC;SAC7C,MAAM,CAAC,KAAK,EAAE,GAAW,EAAE,EAAE;QAC5B,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAc,CAAC;QACxC,MAAM,MAAM,GAAG,iBAAiB,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QAC5D,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QACvC,YAAY,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,346 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { ValidationError } from "../errors.js";
3
+ import { renderList, renderObject } from "../output/render.js";
4
+ import { createQuireClient } from "../quire-client.js";
5
+ import { confirmDestructive } from "../util/confirm.js";
6
+ import { buildFieldBody, FIELD_FIELDS } from "../util/field-flags.js";
7
+ import { resolveTextInput } from "../util/text-input.js";
8
+ const PROJECT_GET_FIELDS = [
9
+ { label: "Name", get: (p) => p.nameText ?? p.name },
10
+ { label: "ID", get: (p) => p.id },
11
+ { label: "OID", get: (p) => p.oid },
12
+ { label: "Description", get: (p) => p.descriptionText },
13
+ { label: "URL", get: (p) => p.url },
14
+ { label: "Start", get: (p) => p.start },
15
+ { label: "Due", get: (p) => p.due },
16
+ { label: "Archived at", get: (p) => p.archivedAt },
17
+ { label: "Public at", get: (p) => p.publicAt },
18
+ ];
19
+ const APPROVAL_CATEGORY_FIELDS = [
20
+ { label: "ID", get: (c) => c.id },
21
+ { label: "Name", get: (c) => c.name },
22
+ {
23
+ label: "Claimers",
24
+ get: (c) => c.claimers === undefined ? "(anyone)" : c.claimers.length === 0 ? "(admins only)" : c.claimers.join(", "),
25
+ },
26
+ {
27
+ label: "Approvers",
28
+ get: (c) => c.approvers === undefined ? "(anyone)" : c.approvers.length === 0 ? "(admins only)" : c.approvers.join(", "),
29
+ },
30
+ ];
31
+ const append = (val, prev) => [...(prev ?? []), val];
32
+ export function registerProjectCommand(program) {
33
+ const project = program.command("project").description("Quire projects.");
34
+ project
35
+ .command("list")
36
+ .description("List projects you can see, optionally scoped to one organization.")
37
+ .option("--org <id>", "Restrict to one organization (id = OID, slug, or URL).")
38
+ .action(async (cmdOpts) => {
39
+ const root = program.opts();
40
+ const client = createQuireClient({ profile: root.profile });
41
+ const projects = cmdOpts.org
42
+ ? await client.listProjectsByOrg(await client.resolveOrgOid(cmdOpts.org))
43
+ : await client.listProjects();
44
+ renderList(projects, root, {
45
+ columns: [
46
+ { header: "ID", get: (p) => p.id },
47
+ { header: "NAME", get: (p) => p.nameText ?? p.name },
48
+ { header: "OID", get: (p) => p.oid },
49
+ ],
50
+ toId: (p) => p.oid,
51
+ });
52
+ });
53
+ project
54
+ .command("get <id>")
55
+ .description("Show project details. <id> = OID, slug, or full URL.")
56
+ .action(async (id) => {
57
+ const root = program.opts();
58
+ const client = createQuireClient({ profile: root.profile });
59
+ const oid = await client.resolveProjectOid(id);
60
+ const p = await client.getProject(oid);
61
+ renderObject(p, root, { fields: PROJECT_GET_FIELDS, toId: (p) => p.oid });
62
+ });
63
+ project
64
+ .command("update <id>")
65
+ .description("Update project metadata. Pass 'null' to --start / --due to clear.")
66
+ .option("--name <name>", "New name")
67
+ .option("--description <text>", "New description ('-' = stdin, '@file' = file)")
68
+ .option("--start <date>", "Start date or 'null' to clear")
69
+ .option("--due <date>", "Due date or 'null' to clear")
70
+ .option("--archive", "Archive the project")
71
+ .option("--unarchive", "Unarchive the project")
72
+ .option("--public", "Make the project public to the organization")
73
+ .option("--private", "Make the project private")
74
+ .option("--add-follower <user>", "Add follower; repeat for multiple", append, [])
75
+ .option("--remove-follower <user>", "Remove follower; repeat for multiple", append, [])
76
+ .action(async (id, cmdOpts) => {
77
+ const root = program.opts();
78
+ const client = createQuireClient({ profile: root.profile });
79
+ if (cmdOpts.archive === true && cmdOpts.unarchive === true) {
80
+ throw new ValidationError("Cannot combine --archive and --unarchive.");
81
+ }
82
+ if (cmdOpts.public === true && cmdOpts.private === true) {
83
+ throw new ValidationError("Cannot combine --public and --private.");
84
+ }
85
+ const oid = await client.resolveProjectOid(id);
86
+ const description = cmdOpts.description !== undefined ? await resolveTextInput(cmdOpts.description) : undefined;
87
+ const body = {};
88
+ if (cmdOpts.name !== undefined)
89
+ body.name = cmdOpts.name;
90
+ if (description !== undefined)
91
+ body.description = description;
92
+ if (cmdOpts.start !== undefined)
93
+ body.start = cmdOpts.start === "null" ? null : cmdOpts.start;
94
+ if (cmdOpts.due !== undefined)
95
+ body.due = cmdOpts.due === "null" ? null : cmdOpts.due;
96
+ if (cmdOpts.archive === true)
97
+ body.archived = true;
98
+ if (cmdOpts.unarchive === true)
99
+ body.archived = false;
100
+ if (cmdOpts.public === true)
101
+ body.public = true;
102
+ if (cmdOpts.private === true)
103
+ body.public = false;
104
+ if ((cmdOpts.addFollower?.length ?? 0) > 0)
105
+ body.addFollowers = cmdOpts.addFollower;
106
+ if ((cmdOpts.removeFollower?.length ?? 0) > 0)
107
+ body.removeFollowers = cmdOpts.removeFollower;
108
+ if (Object.keys(body).length === 0) {
109
+ throw new ValidationError("`project update` requires at least one of --name / --description / --start / --due / --archive / --unarchive / --public / --private / --add-follower / --remove-follower.");
110
+ }
111
+ const p = await client.updateProject(oid, body);
112
+ renderObject(p, root, { fields: PROJECT_GET_FIELDS, toId: (p) => p.oid });
113
+ });
114
+ project
115
+ .command("members <id>")
116
+ .description("List members of a project. <id> = OID, slug, or URL.")
117
+ .action(async (id) => {
118
+ const root = program.opts();
119
+ const client = createQuireClient({ profile: root.profile });
120
+ const oid = await client.resolveProjectOid(id);
121
+ const members = await client.listProjectMembers(oid);
122
+ renderList(members, root, {
123
+ columns: [
124
+ { header: "NAME", get: (m) => m.nameText ?? m.name },
125
+ { header: "EMAIL", get: (m) => m.email ?? "" },
126
+ { header: "OID", get: (m) => m.oid },
127
+ ],
128
+ toId: (m) => m.oid,
129
+ });
130
+ });
131
+ project
132
+ .command("export <id>")
133
+ .description("Export the entire project as CSV (default) or JSON. Writes to stdout unless --output is set.")
134
+ .option("--format <fmt>", "Export format: csv | json", "csv")
135
+ .option("--output <file>", "Write to this path instead of stdout (use '-' for stdout, the default)")
136
+ .action(async (id, cmdOpts) => {
137
+ const root = program.opts();
138
+ const client = createQuireClient({ profile: root.profile });
139
+ if (cmdOpts.format !== "csv" && cmdOpts.format !== "json") {
140
+ throw new ValidationError("--format must be one of: csv, json");
141
+ }
142
+ const oid = await client.resolveProjectOid(id);
143
+ const body = cmdOpts.format === "csv"
144
+ ? await client.exportProjectCsv(oid)
145
+ : await client.exportProjectJson(oid);
146
+ if (cmdOpts.output !== undefined && cmdOpts.output !== "-") {
147
+ writeFileSync(cmdOpts.output, body);
148
+ if (root.quiet === true) {
149
+ process.stdout.write(`${cmdOpts.output}\n`);
150
+ }
151
+ else if (root.json !== true) {
152
+ process.stderr.write(`Wrote ${body.length} bytes to ${cmdOpts.output}.\n`);
153
+ }
154
+ }
155
+ else {
156
+ process.stdout.write(body);
157
+ if (!body.endsWith("\n"))
158
+ process.stdout.write("\n");
159
+ }
160
+ });
161
+ // -------- Approval categories (Phase 5.3 slice B) --------
162
+ const approvalCategory = project
163
+ .command("approval-category")
164
+ .description("Manage approval categories (gates that group approvers / claimers per project).");
165
+ approvalCategory
166
+ .command("add <project>")
167
+ .description("Add an approval category to a project.")
168
+ .requiredOption("--id <id>", "Caller-supplied id (must pass Quire's isValidId; '' is the implicit Default)")
169
+ .requiredOption("--name <name>", "Category name")
170
+ .option("--claimer <user>", "Claimer (OID, id, or email); repeat for multiple. Omit for 'anyone'.", append, [])
171
+ .option("--approver <user>", "Approver; repeat for multiple. Omit for 'anyone'.", append, [])
172
+ .option("--claimers-admins-only", "Restrict claimers to admins (overrides --claimer)")
173
+ .option("--approvers-admins-only", "Restrict approvers to admins (overrides --approver)")
174
+ .action(async (projectInput, cmdOpts) => {
175
+ const root = program.opts();
176
+ const client = createQuireClient({ profile: root.profile });
177
+ const projectOid = await client.resolveProjectOid(projectInput);
178
+ const claimers = cmdOpts.claimersAdminsOnly === true
179
+ ? []
180
+ : (cmdOpts.claimer?.length ?? 0) > 0 ? cmdOpts.claimer : undefined;
181
+ const approvers = cmdOpts.approversAdminsOnly === true
182
+ ? []
183
+ : (cmdOpts.approver?.length ?? 0) > 0 ? cmdOpts.approver : undefined;
184
+ const c = await client.addProjectApprovalCategory(projectOid, {
185
+ id: cmdOpts.id,
186
+ name: cmdOpts.name,
187
+ ...(claimers !== undefined ? { claimers } : {}),
188
+ ...(approvers !== undefined ? { approvers } : {}),
189
+ });
190
+ renderObject(c, root, { fields: APPROVAL_CATEGORY_FIELDS, toId: (c) => c.id });
191
+ });
192
+ approvalCategory
193
+ .command("update <project> <category-id>")
194
+ .description("Update an approval category. --claimers-anyone / --approvers-anyone resets to 'anyone'.")
195
+ .option("--name <name>", "New name")
196
+ .option("--claimer <user>", "Replace claimer list; repeat for multiple", append, [])
197
+ .option("--approver <user>", "Replace approver list; repeat for multiple", append, [])
198
+ .option("--claimers-admins-only", "Restrict claimers to admins (empty list)")
199
+ .option("--approvers-admins-only", "Restrict approvers to admins (empty list)")
200
+ .option("--claimers-anyone", "Reset claimers to anyone (clears the list)")
201
+ .option("--approvers-anyone", "Reset approvers to anyone (clears the list)")
202
+ .action(async (projectInput, categoryId, cmdOpts) => {
203
+ const root = program.opts();
204
+ const client = createQuireClient({ profile: root.profile });
205
+ const projectOid = await client.resolveProjectOid(projectInput);
206
+ let claimers;
207
+ if (cmdOpts.claimersAnyone === true)
208
+ claimers = null;
209
+ else if (cmdOpts.claimersAdminsOnly === true)
210
+ claimers = [];
211
+ else if ((cmdOpts.claimer?.length ?? 0) > 0)
212
+ claimers = cmdOpts.claimer;
213
+ let approvers;
214
+ if (cmdOpts.approversAnyone === true)
215
+ approvers = null;
216
+ else if (cmdOpts.approversAdminsOnly === true)
217
+ approvers = [];
218
+ else if ((cmdOpts.approver?.length ?? 0) > 0)
219
+ approvers = cmdOpts.approver;
220
+ const body = {};
221
+ if (cmdOpts.name !== undefined)
222
+ body.name = cmdOpts.name;
223
+ if (claimers !== undefined)
224
+ body.claimers = claimers;
225
+ if (approvers !== undefined)
226
+ body.approvers = approvers;
227
+ const c = await client.updateProjectApprovalCategory(projectOid, categoryId, body);
228
+ renderObject(c, root, { fields: APPROVAL_CATEGORY_FIELDS, toId: (c) => c.id });
229
+ });
230
+ approvalCategory
231
+ .command("remove <project> <category-id>")
232
+ .description("Remove an approval category from a project. Prompts unless --yes.")
233
+ .action(async (projectInput, categoryId) => {
234
+ const root = program.opts();
235
+ const client = createQuireClient({ profile: root.profile });
236
+ const projectOid = await client.resolveProjectOid(projectInput);
237
+ await confirmDestructive({
238
+ question: `Remove approval category "${categoryId}" from project ${projectOid}? Tasks using this category will fall back to the default.`,
239
+ yes: root.yes,
240
+ });
241
+ await client.removeProjectApprovalCategory(projectOid, categoryId);
242
+ if (root.json === true) {
243
+ process.stdout.write(`${JSON.stringify({ project: projectOid, category: categoryId, removed: true })}\n`);
244
+ }
245
+ else if (root.quiet === true) {
246
+ process.stdout.write(`${categoryId}\n`);
247
+ }
248
+ else {
249
+ process.stderr.write(`Removed approval category "${categoryId}" from project ${projectOid}.\n`);
250
+ }
251
+ });
252
+ // -------- Custom field definitions (Phase 5.3 slice C) --------
253
+ const field = project
254
+ .command("field")
255
+ .description("Manage custom-field definitions on a project.");
256
+ field
257
+ .command("add <project>")
258
+ .description("Add a custom-field definition. Use --extra for type-specific config (JSON-parsed when possible).")
259
+ .requiredOption("--name <name>", "Field name (required)")
260
+ .requiredOption("--type <type>", "Field type: text|number|money|date|duration|select|checkbox|user|task|hyperlink|email|formula|file|lookup")
261
+ .option("--hidden", "Hide the field by default")
262
+ .option("--private", "Mark as private (visible only to admins)")
263
+ .option("--percent", "Format numeric values as percentage")
264
+ .option("--multiple", "Allow multiple values")
265
+ .option("--clear-on-dup", "Clear value when a task is duplicated")
266
+ .option("--extra <kv>", "key=value; repeat for type-specific config (e.g. --extra options='[{...}]')", append, [])
267
+ .action(async (projectInput, cmdOpts) => {
268
+ const root = program.opts();
269
+ const client = createQuireClient({ profile: root.profile });
270
+ const projectOid = await client.resolveProjectOid(projectInput);
271
+ const body = buildFieldBody(cmdOpts);
272
+ const f = await client.addProjectField(projectOid, body);
273
+ renderObject(f, root, { fields: FIELD_FIELDS, toId: (f) => f.name });
274
+ });
275
+ field
276
+ .command("update <project> <field-name>")
277
+ .description("Update a custom-field definition. Use --rename for renaming (or `field rename` for the dedicated command).")
278
+ .option("--type <type>", "New type")
279
+ .option("--hidden")
280
+ .option("--private")
281
+ .option("--percent")
282
+ .option("--multiple")
283
+ .option("--clear-on-dup")
284
+ .option("--extra <kv>", "key=value; repeat for type-specific config", append, [])
285
+ .action(async (projectInput, fieldName, cmdOpts) => {
286
+ const root = program.opts();
287
+ const client = createQuireClient({ profile: root.profile });
288
+ const projectOid = await client.resolveProjectOid(projectInput);
289
+ const body = buildFieldBody(cmdOpts);
290
+ if (Object.keys(body).length === 0) {
291
+ throw new ValidationError("`field update` requires at least one of --type / --hidden / --private / --percent / --multiple / --clear-on-dup / --extra.");
292
+ }
293
+ const f = await client.updateProjectField(projectOid, fieldName, body);
294
+ renderObject(f, root, { fields: FIELD_FIELDS, toId: (f) => f.name });
295
+ });
296
+ field
297
+ .command("rename <project> <field-name>")
298
+ .description("Rename a custom-field definition.")
299
+ .requiredOption("--new-name <new-name>", "New field name")
300
+ .action(async (projectInput, fieldName, cmdOpts) => {
301
+ const root = program.opts();
302
+ const client = createQuireClient({ profile: root.profile });
303
+ const projectOid = await client.resolveProjectOid(projectInput);
304
+ const f = await client.renameProjectField(projectOid, fieldName, cmdOpts.newName);
305
+ renderObject(f, root, { fields: FIELD_FIELDS, toId: (f) => f.name });
306
+ });
307
+ field
308
+ .command("move <project> <field-name>")
309
+ .description("Reorder a custom-field definition. --before <field-name> places it just before that field; --to-end puts it at the end.")
310
+ .option("--before <field-name>", "Move just before this field")
311
+ .option("--to-end", "Move to the end of the field list")
312
+ .action(async (projectInput, fieldName, cmdOpts) => {
313
+ const root = program.opts();
314
+ const client = createQuireClient({ profile: root.profile });
315
+ const projectOid = await client.resolveProjectOid(projectInput);
316
+ if (cmdOpts.before !== undefined && cmdOpts.toEnd === true) {
317
+ throw new ValidationError("Cannot combine --before and --to-end.");
318
+ }
319
+ const before = cmdOpts.toEnd === true ? null : cmdOpts.before;
320
+ const f = await client.moveProjectField(projectOid, fieldName, before);
321
+ renderObject(f, root, { fields: FIELD_FIELDS, toId: (f) => f.name });
322
+ });
323
+ field
324
+ .command("remove <project> <field-name>")
325
+ .description("Remove a custom-field definition. Prompts unless --yes.")
326
+ .action(async (projectInput, fieldName) => {
327
+ const root = program.opts();
328
+ const client = createQuireClient({ profile: root.profile });
329
+ const projectOid = await client.resolveProjectOid(projectInput);
330
+ await confirmDestructive({
331
+ question: `Remove custom-field "${fieldName}" from project ${projectOid}? Existing values on tasks will be dropped.`,
332
+ yes: root.yes,
333
+ });
334
+ await client.removeProjectField(projectOid, fieldName);
335
+ if (root.json === true) {
336
+ process.stdout.write(`${JSON.stringify({ project: projectOid, field: fieldName, removed: true })}\n`);
337
+ }
338
+ else if (root.quiet === true) {
339
+ process.stdout.write(`${fieldName}\n`);
340
+ }
341
+ else {
342
+ process.stderr.write(`Removed custom-field "${fieldName}" from project ${projectOid}.\n`);
343
+ }
344
+ });
345
+ }
346
+ //# sourceMappingURL=project.js.map