@uic-coe-connect/cli 0.10.0 → 0.11.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.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The directory from the terminal.
3
+ *
4
+ * Two audiences. Somebody curating the directory wants `list`, `show` and
5
+ * `add`. Somebody *integrating* an app wants `version` and `changes`, which
6
+ * show exactly what that app's mirror polls and exactly what it would receive —
7
+ * the questions that are otherwise answered by adding console.log to the SDK.
8
+ */
9
+ export declare function registerDirectoryCommands(): void;
@@ -0,0 +1,173 @@
1
+ import { details, emit, info, table } from "../output.js";
2
+ import { register } from "../registry.js";
3
+ const when = (iso) => (iso ? iso.slice(0, 16).replace("T", " ") : "—");
4
+ /**
5
+ * The directory from the terminal.
6
+ *
7
+ * Two audiences. Somebody curating the directory wants `list`, `show` and
8
+ * `add`. Somebody *integrating* an app wants `version` and `changes`, which
9
+ * show exactly what that app's mirror polls and exactly what it would receive —
10
+ * the questions that are otherwise answered by adding console.log to the SDK.
11
+ */
12
+ export function registerDirectoryCommands() {
13
+ register({
14
+ name: "directory list",
15
+ summary: "People in the shared directory",
16
+ usage: "directory list [--search <q>] [--org <code>] [--status <s>] [--source <s>] [--limit <n>]",
17
+ details: [
18
+ " --search Substring match on netid, name or email.",
19
+ " --org One of BME, CHE, CME, CS, ECE, MIE, COE. Repeatable as a comma list.",
20
+ " --status active (default) | inactive | all.",
21
+ " --source login | manual | app — how the record got here.",
22
+ " --limit Default 50.",
23
+ ],
24
+ async run({ args, client }) {
25
+ const params = new URLSearchParams();
26
+ for (const [flag, key] of [
27
+ ["search", "search"],
28
+ ["org", "organizations"],
29
+ ["status", "status"],
30
+ ["source", "source"],
31
+ ["limit", "limit"],
32
+ ]) {
33
+ const v = args.flag(flag);
34
+ if (v)
35
+ params.set(key, v);
36
+ }
37
+ if (!params.has("limit"))
38
+ params.set("limit", "50");
39
+ const data = await client().request("GET", `/directory/users?${params}`);
40
+ emit(data, () => {
41
+ if (data.users.length === 0) {
42
+ info("Nobody matches.");
43
+ return;
44
+ }
45
+ table(data.users.map((u) => ({
46
+ netid: u.netid,
47
+ name: u.displayName,
48
+ org: u.organization ?? "—",
49
+ email: u.email ?? "—",
50
+ status: u.active ? "active" : u.masked ? "inactive (masked)" : "inactive",
51
+ from: u.source,
52
+ })), ["netid", "name", "org", "email", "status", "from"]);
53
+ info("");
54
+ info(`${data.users.length} of ${data.total}.`);
55
+ });
56
+ },
57
+ }, {
58
+ name: "directory show",
59
+ summary: "One person's full directory record",
60
+ usage: "directory show <netid>",
61
+ async run({ args, client }) {
62
+ const netid = args.arg(0, "netid");
63
+ const data = await client().request("GET", `/directory/users?search=${encodeURIComponent(netid)}&status=all&limit=50`);
64
+ const user = data.users.find((u) => u.netid === netid);
65
+ if (!user)
66
+ throw new Error(`${netid} is not in the directory`);
67
+ emit({ user }, () => {
68
+ details([
69
+ ["NetID", user.netid],
70
+ ["Name", user.displayName],
71
+ ["Email", user.email ?? "—"],
72
+ ["UIN", user.uin ?? "—"],
73
+ ["Organization", user.organization ?? "—"],
74
+ ["Affiliations", user.affiliations.length > 0 ? user.affiliations.join(", ") : "—"],
75
+ ["Status", user.active ? "active" : "inactive"],
76
+ ["Added", `${user.source}${user.createdByApp ? ` (${user.createdByApp})` : ""}`],
77
+ ["Created", when(user.createdAt)],
78
+ ["Updated", when(user.updatedAt)],
79
+ ["Last seen", when(user.lastSeen)],
80
+ ]);
81
+ if (!user.active) {
82
+ info("");
83
+ info(user.masked
84
+ ? `Masked: apps see "Disabled User", not ${user.displayName}.`
85
+ : `Not masked: apps still see ${user.displayName}.`);
86
+ }
87
+ if (user.affiliations.length === 0) {
88
+ info("");
89
+ info("No affiliations — they have never signed in. Do not gate access on that field.");
90
+ }
91
+ });
92
+ },
93
+ }, {
94
+ name: "directory add",
95
+ summary: "Add somebody to the shared directory",
96
+ usage: "directory add <netid> --name <full name> [--email <e>] [--org <code>] [--uin <u>]",
97
+ details: [
98
+ " --name Required for somebody new. Ignored if they are already here.",
99
+ " --org One of BME, CHE, CME, CS, ECE, MIE, COE.",
100
+ "",
101
+ " Adding somebody who already exists changes nothing and is not an error.",
102
+ ],
103
+ async run({ args, client }) {
104
+ const netid = args.arg(0, "netid");
105
+ const body = { netid };
106
+ for (const [flag, key] of [
107
+ ["name", "displayName"],
108
+ ["email", "email"],
109
+ ["org", "organization"],
110
+ ["uin", "uin"],
111
+ ]) {
112
+ const v = args.flag(flag);
113
+ if (v)
114
+ body[key] = v;
115
+ }
116
+ const data = await client().request("POST", "/directory/users", body);
117
+ emit(data, () => info(`Added ${data.user.displayName} (${data.user.netid}).`));
118
+ },
119
+ }, {
120
+ name: "directory version",
121
+ summary: "What an app's mirror polls — generation, count, last change",
122
+ usage: "directory version",
123
+ details: [
124
+ " The generation is opaque and changes whenever anything in the directory",
125
+ " changes. A mirror that already holds this generation does no work.",
126
+ ],
127
+ async run({ client }) {
128
+ const data = await client().request("GET", "/directory/version");
129
+ emit(data, () => details([
130
+ ["Generation", data.generation],
131
+ ["People", String(data.count)],
132
+ ["Last change", when(data.updatedAt)],
133
+ ]));
134
+ },
135
+ }, {
136
+ name: "directory changes",
137
+ summary: "Exactly what a mirror would receive since a given time",
138
+ usage: "directory changes [--since <iso>] [--limit <n>]",
139
+ details: [
140
+ " --since ISO-8601. Omit it to see a first full sync.",
141
+ "",
142
+ " This is the app-facing projection, so it is what actually lands in",
143
+ " coe_users — no UIN, and deactivated people masked unless excepted.",
144
+ " Use it to check an integration without instrumenting the SDK.",
145
+ ],
146
+ async run({ args, client }) {
147
+ const params = new URLSearchParams();
148
+ params.set("updatedSince", args.flag("since") ?? "");
149
+ params.set("limit", args.flag("limit") ?? "500");
150
+ const data = await client().request("GET", `/directory/changes?${params}`);
151
+ emit(data, () => {
152
+ if (data.users.length === 0) {
153
+ info("Nothing has changed since then.");
154
+ }
155
+ else {
156
+ table(data.users.map((u) => ({
157
+ netid: u.netid,
158
+ name: u.displayName,
159
+ org: u.organization ?? "—",
160
+ email: u.email ?? "—",
161
+ active: u.active ? "yes" : "no",
162
+ changed: when(u.updatedAt),
163
+ })), ["netid", "name", "org", "email", "active", "changed"]);
164
+ info("");
165
+ info(`${data.users.length} of ${data.total} changed rows.`);
166
+ }
167
+ if (data.deleted.length > 0)
168
+ info(`Removed: ${data.deleted.join(", ")}`);
169
+ info(`Generation ${data.version.generation}, ${data.version.count} people.`);
170
+ });
171
+ },
172
+ });
173
+ }
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { registerAccessCommands } from "./commands/access.js";
4
4
  import { registerAppCommands } from "./commands/apps.js";
5
5
  import { registerAuthCommands } from "./commands/auth.js";
6
6
  import { registerDeployCommands } from "./commands/deploy.js";
7
+ import { registerDirectoryCommands } from "./commands/directory.js";
7
8
  import { registerEnvCommands } from "./commands/env.js";
8
9
  import { registerEventCommands } from "./commands/events.js";
9
10
  import { registerIntegrationCommands } from "./commands/integration.js";
@@ -26,6 +27,7 @@ registerResourceCommands();
26
27
  registerLogCommands();
27
28
  registerVmCommands();
28
29
  registerReportCommands();
30
+ registerDirectoryCommands();
29
31
  function parseArgs(argv) {
30
32
  const positional = [];
31
33
  const flags = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uic-coe-connect/cli",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "The coe CLI \u2014 manage COEConnect apps (pipelines, deploys, access, env) from a terminal, with browser-approved auth. Designed to be driven by an AI agent.",
5
5
  "type": "module",
6
6
  "bin": {