@uic-coe-connect/cli 0.9.0 → 0.10.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/commands/apps.js +2 -0
- package/dist/commands/events.d.ts +8 -0
- package/dist/commands/events.js +135 -0
- package/dist/commands/integration.d.ts +1 -0
- package/dist/commands/integration.js +193 -0
- package/dist/commands/reports.js +24 -0
- package/dist/index.js +4 -0
- package/dist/types.d.ts +10 -0
- package/package.json +2 -2
package/dist/commands/apps.js
CHANGED
|
@@ -50,6 +50,8 @@ export function registerAppCommands() {
|
|
|
50
50
|
details([
|
|
51
51
|
["Id", app.id],
|
|
52
52
|
["Name", app.name],
|
|
53
|
+
["Description", app.description || "(none — required; set it before other edits)"],
|
|
54
|
+
["Visibility", app.stage === "dev" ? "development (dev team only)" : "live"],
|
|
53
55
|
["VM", app.vmId ?? "—"],
|
|
54
56
|
["URL", app.url],
|
|
55
57
|
["Repo", app.repo ?? "—"],
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events and alarms from the terminal.
|
|
3
|
+
*
|
|
4
|
+
* The read side matters most here: an agent investigating "why is this app
|
|
5
|
+
* behaving oddly" wants the event names, a series, and which alarms are
|
|
6
|
+
* currently firing, and none of that should require a browser.
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerEventCommands(): void;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { details, emit, info, table } from "../output.js";
|
|
2
|
+
import { register } from "../registry.js";
|
|
3
|
+
import { resolveApp } from "./apps.js";
|
|
4
|
+
/**
|
|
5
|
+
* Events and alarms from the terminal.
|
|
6
|
+
*
|
|
7
|
+
* The read side matters most here: an agent investigating "why is this app
|
|
8
|
+
* behaving oddly" wants the event names, a series, and which alarms are
|
|
9
|
+
* currently firing, and none of that should require a browser.
|
|
10
|
+
*/
|
|
11
|
+
export function registerEventCommands() {
|
|
12
|
+
register({
|
|
13
|
+
name: "events list",
|
|
14
|
+
summary: "Event names an app reports, and anything refused today",
|
|
15
|
+
usage: "events list <app>",
|
|
16
|
+
async run({ args, client }) {
|
|
17
|
+
const c = client();
|
|
18
|
+
const app = await resolveApp(c, args.arg(0, "app"));
|
|
19
|
+
const data = await c.request("GET", `/registered-apps/${app.id}/events/names`);
|
|
20
|
+
emit(data, () => {
|
|
21
|
+
if (data.names.length === 0) {
|
|
22
|
+
info(`${app.id} has not reported any events.`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
table(data.names.map((n) => ({
|
|
26
|
+
name: n.name,
|
|
27
|
+
"first seen": n.firstSeen.slice(0, 10),
|
|
28
|
+
"last seen": n.lastSeen.slice(0, 16).replace("T", " "),
|
|
29
|
+
})), ["name", "first seen", "last seen"]);
|
|
30
|
+
}
|
|
31
|
+
const dropped = Object.values(data.drops).reduce((a, b) => a + b, 0);
|
|
32
|
+
if (dropped > 0) {
|
|
33
|
+
info("");
|
|
34
|
+
info(`${dropped} refused today — rate ${data.drops.rate ?? 0}, ` +
|
|
35
|
+
`over the name cap ${data.drops.names ?? 0}, unusable name ${data.drops.invalid ?? 0}.`);
|
|
36
|
+
}
|
|
37
|
+
info("");
|
|
38
|
+
info(`Limits: ${data.limits.ratePerMin}/min, ${data.limits.maxNames} distinct names, 30-day retention.`);
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
}, {
|
|
42
|
+
name: "events series",
|
|
43
|
+
summary: "One event name as a time series",
|
|
44
|
+
usage: "events series <app> <name> [--days <n>] [--bucket <min>] [--agg <a>]",
|
|
45
|
+
details: [
|
|
46
|
+
" --days How far back. Default 7, max 30.",
|
|
47
|
+
" --bucket Bucket width in minutes. Default 60.",
|
|
48
|
+
" --agg count | sum | avg | max | min. Default count.",
|
|
49
|
+
],
|
|
50
|
+
async run({ args, client }) {
|
|
51
|
+
const c = client();
|
|
52
|
+
const app = await resolveApp(c, args.arg(0, "app"));
|
|
53
|
+
const name = args.arg(1, "name");
|
|
54
|
+
const params = new URLSearchParams({ name });
|
|
55
|
+
for (const [flag, key] of [["days", "days"], ["bucket", "bucket"], ["agg", "agg"]]) {
|
|
56
|
+
const v = args.flag(flag);
|
|
57
|
+
if (v)
|
|
58
|
+
params.set(key, v);
|
|
59
|
+
}
|
|
60
|
+
const data = await c.request("GET", `/registered-apps/${app.id}/events/series?${params}`);
|
|
61
|
+
emit(data, () => {
|
|
62
|
+
if (data.points.length === 0) {
|
|
63
|
+
info(`No "${name}" events in that window.`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
table(data.points.map((p) => ({
|
|
67
|
+
when: p.ts.slice(0, 16).replace("T", " "),
|
|
68
|
+
value: String(Math.round(p.value * 100) / 100),
|
|
69
|
+
})), ["when", "value"]);
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
}, {
|
|
73
|
+
name: "events recent",
|
|
74
|
+
summary: "The most recent raw events",
|
|
75
|
+
usage: "events recent <app> [--name <event>] [--limit <n>]",
|
|
76
|
+
async run({ args, client }) {
|
|
77
|
+
const c = client();
|
|
78
|
+
const app = await resolveApp(c, args.arg(0, "app"));
|
|
79
|
+
const params = new URLSearchParams();
|
|
80
|
+
if (args.flag("name"))
|
|
81
|
+
params.set("name", args.flag("name"));
|
|
82
|
+
if (args.flag("limit"))
|
|
83
|
+
params.set("limit", args.flag("limit"));
|
|
84
|
+
const data = await c.request("GET", `/registered-apps/${app.id}/events/recent?${params}`);
|
|
85
|
+
emit(data, () => table(data.events.map((e) => ({
|
|
86
|
+
when: e.ts.slice(0, 19).replace("T", " "),
|
|
87
|
+
name: e.name,
|
|
88
|
+
value: String(e.value),
|
|
89
|
+
props: e.props ? JSON.stringify(e.props) : "",
|
|
90
|
+
})), ["when", "name", "value", "props"]));
|
|
91
|
+
},
|
|
92
|
+
}, {
|
|
93
|
+
name: "alarms list",
|
|
94
|
+
summary: "An app's alarms and what each is currently reading",
|
|
95
|
+
usage: "alarms list <app>",
|
|
96
|
+
async run({ args, client }) {
|
|
97
|
+
const c = client();
|
|
98
|
+
const app = await resolveApp(c, args.arg(0, "app"));
|
|
99
|
+
const data = await c.request("GET", `/registered-apps/${app.id}/alarms`);
|
|
100
|
+
emit(data, () => {
|
|
101
|
+
if (data.alarms.length === 0) {
|
|
102
|
+
info(`${app.id} has no alarms. Add one in the portal's Events tab.`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
table(data.alarms.map((a) => ({
|
|
106
|
+
state: a.enabled ? a.state : "off",
|
|
107
|
+
name: a.name,
|
|
108
|
+
condition: a.description,
|
|
109
|
+
now: a.lastValue === null ? "—" : String(Math.round(a.lastValue * 100) / 100),
|
|
110
|
+
emails: a.recipients.join(","),
|
|
111
|
+
})), ["state", "name", "condition", "now", "emails"]);
|
|
112
|
+
const firing = data.alarms.filter((a) => a.enabled && a.state === "firing");
|
|
113
|
+
if (firing.length > 0) {
|
|
114
|
+
info("");
|
|
115
|
+
info(`${firing.length} firing: ${firing.map((a) => a.name).join(", ")}`);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
}, {
|
|
120
|
+
name: "alarms check",
|
|
121
|
+
summary: "Evaluate one alarm now, without emailing anyone",
|
|
122
|
+
usage: "alarms check <app> <alarm-id>",
|
|
123
|
+
async run({ args, client }) {
|
|
124
|
+
const c = client();
|
|
125
|
+
const app = await resolveApp(c, args.arg(0, "app"));
|
|
126
|
+
const id = args.arg(1, "alarm-id");
|
|
127
|
+
const data = await c.request("POST", `/registered-apps/${app.id}/alarms/${id}/evaluate`);
|
|
128
|
+
emit(data, () => details([
|
|
129
|
+
["Condition", data.description],
|
|
130
|
+
["Value", data.measurable ? String(Math.round((data.value ?? 0) * 100) / 100) : "not measurable"],
|
|
131
|
+
["Would fire", data.wouldFire ? "yes" : "no"],
|
|
132
|
+
]));
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function registerIntegrationCommands(): void;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { details, emit, info } from "../output.js";
|
|
2
|
+
import { register } from "../registry.js";
|
|
3
|
+
import { resolveApp } from "./apps.js";
|
|
4
|
+
/**
|
|
5
|
+
* Everything the SDK reads, and what each one turns on. The CLI knows the
|
|
6
|
+
* *names* rather than the values — env values live behind their own route on
|
|
7
|
+
* purpose (see `coe env`), and an integration check should never be the thing
|
|
8
|
+
* that prints a secret into a terminal scrollback.
|
|
9
|
+
*/
|
|
10
|
+
const REQUIRED_ENV = [
|
|
11
|
+
{ key: "SHIB_MODE", enables: "real SAML instead of the dev login form" },
|
|
12
|
+
{ key: "SHIB_DOMAIN", enables: "the app's own hostname, for the SP endpoints" },
|
|
13
|
+
{ key: "SHIB_VM_HOST", enables: "which VM's SP entity the app authenticates under" },
|
|
14
|
+
{ key: "SESSION_SECRET", enables: "signing the session cookie" },
|
|
15
|
+
{ key: "COE_URL", enables: "reaching COEConnect" },
|
|
16
|
+
{ key: "COE_APP_ID", enables: "identifying this app to COEConnect" },
|
|
17
|
+
{ key: "COE_APP_TOKEN", enables: "access enforcement and login reporting" },
|
|
18
|
+
{
|
|
19
|
+
key: "SHIB_PRIVATE_KEY_PATH",
|
|
20
|
+
enables: "a non-default SP key path",
|
|
21
|
+
optional: true,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
/** The legacy fleet-wide credentials, still honoured but worth moving off. */
|
|
25
|
+
const LEGACY_ENV = ["COE_ACCESS_TOKEN", "COE_EVENTS_TOKEN", "COE_NOTIFY_TOKEN"];
|
|
26
|
+
export function registerIntegrationCommands() {
|
|
27
|
+
register({
|
|
28
|
+
name: "integration show",
|
|
29
|
+
summary: "Full integration status for an app — what's wired up and what's missing",
|
|
30
|
+
usage: "integration show <app>",
|
|
31
|
+
async run({ args, client }) {
|
|
32
|
+
const c = client();
|
|
33
|
+
const app = await resolveApp(c, args.arg(0, "app"));
|
|
34
|
+
const token = await c
|
|
35
|
+
.request("GET", `/registered-apps/${app.id}/token`)
|
|
36
|
+
.then((r) => r.token)
|
|
37
|
+
.catch(() => null);
|
|
38
|
+
const usage = await c
|
|
39
|
+
.request("GET", `/registered-apps/${app.id}/logins?days=30`)
|
|
40
|
+
.catch(() => null);
|
|
41
|
+
const envKeys = new Set(app.envKeys ?? []);
|
|
42
|
+
const missing = REQUIRED_ENV.filter((e) => !e.optional && !envKeys.has(e.key));
|
|
43
|
+
const legacy = LEGACY_ENV.filter((k) => envKeys.has(k));
|
|
44
|
+
// Stored env only reaches the VM through a pipeline's `env` step. An
|
|
45
|
+
// app can look perfectly configured here and have none of it on disk,
|
|
46
|
+
// which is the single most common reason an integration "doesn't work".
|
|
47
|
+
const pipelines = app.pipelines ?? [];
|
|
48
|
+
const writesEnv = pipelines.some((p) => p.config.steps.some((s) => s.type === "env"));
|
|
49
|
+
const catalogOnly = app.integration === "catalog";
|
|
50
|
+
const next = nextSteps({
|
|
51
|
+
catalogOnly,
|
|
52
|
+
hasToken: Boolean(token),
|
|
53
|
+
tokenUsed: Boolean(token?.lastUsedAt),
|
|
54
|
+
missing: missing.map((m) => m.key),
|
|
55
|
+
legacy,
|
|
56
|
+
hasPipeline: pipelines.length > 0,
|
|
57
|
+
writesEnv,
|
|
58
|
+
reporting: usage?.reporting ?? false,
|
|
59
|
+
});
|
|
60
|
+
emit({
|
|
61
|
+
appId: app.id,
|
|
62
|
+
integration: app.integration ?? "coeconnect",
|
|
63
|
+
url: app.url,
|
|
64
|
+
vmId: app.vmId,
|
|
65
|
+
token,
|
|
66
|
+
env: {
|
|
67
|
+
present: REQUIRED_ENV.filter((e) => envKeys.has(e.key)).map((e) => e.key),
|
|
68
|
+
missing: missing.map((e) => e.key),
|
|
69
|
+
legacy,
|
|
70
|
+
},
|
|
71
|
+
pipeline: { count: pipelines.length, writesEnv },
|
|
72
|
+
usage: usage && {
|
|
73
|
+
reporting: usage.reporting,
|
|
74
|
+
distinctUsers30d: usage.distinctUsers,
|
|
75
|
+
},
|
|
76
|
+
nextSteps: next,
|
|
77
|
+
}, () => {
|
|
78
|
+
details([
|
|
79
|
+
["App", `${app.id} (${app.url})`],
|
|
80
|
+
["Integration", catalogOnly ? "catalogue entry — the app does not call COEConnect" : "coeconnect"],
|
|
81
|
+
[
|
|
82
|
+
"Service token",
|
|
83
|
+
token
|
|
84
|
+
? `${token.preview}… created ${token.createdAt.slice(0, 10)} by ${token.createdBy}` +
|
|
85
|
+
(token.lastUsedAt ? `, last used ${token.lastUsedAt.slice(0, 16).replace("T", " ")}` : ", NEVER USED")
|
|
86
|
+
: "none — run: coe token generate " + app.id,
|
|
87
|
+
],
|
|
88
|
+
["Env present", REQUIRED_ENV.filter((e) => envKeys.has(e.key)).map((e) => e.key).join(", ") || "(none)"],
|
|
89
|
+
["Env missing", missing.map((e) => e.key).join(", ") || "(none)"],
|
|
90
|
+
["Legacy env", legacy.join(", ") || "(none)"],
|
|
91
|
+
["Pipelines", `${pipelines.length}${pipelines.length > 0 ? (writesEnv ? ", writes .env" : ", NO env step") : ""}`],
|
|
92
|
+
["Login reporting", usage ? (usage.reporting ? `yes — ${usage.distinctUsers} people in 30d` : "no logins reported yet") : "unknown"],
|
|
93
|
+
]);
|
|
94
|
+
if (next.length > 0) {
|
|
95
|
+
info("");
|
|
96
|
+
info("NEXT STEPS");
|
|
97
|
+
for (const step of next)
|
|
98
|
+
info(` • ${step}`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
}, {
|
|
103
|
+
name: "token show",
|
|
104
|
+
summary: "Show an app's service token metadata (never the token itself)",
|
|
105
|
+
usage: "token show <app>",
|
|
106
|
+
async run({ args, client }) {
|
|
107
|
+
const c = client();
|
|
108
|
+
const app = await resolveApp(c, args.arg(0, "app"));
|
|
109
|
+
const { token } = await c.request("GET", `/registered-apps/${app.id}/token`);
|
|
110
|
+
emit({ appId: app.id, token }, () => {
|
|
111
|
+
if (!token) {
|
|
112
|
+
info(`No service token for ${app.id}. Run: coe token generate ${app.id}`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
details([
|
|
116
|
+
["Token", `${token.preview}…`],
|
|
117
|
+
["Created", `${token.createdAt} by ${token.createdBy}`],
|
|
118
|
+
["Last used", token.lastUsedAt ?? "never — the app has not called COEConnect"],
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
}, {
|
|
123
|
+
name: "token generate",
|
|
124
|
+
summary: "Mint a new service token, printing the three env lines an app needs",
|
|
125
|
+
usage: "token generate <app>",
|
|
126
|
+
async run({ args, client }) {
|
|
127
|
+
const c = client();
|
|
128
|
+
const app = await resolveApp(c, args.arg(0, "app"));
|
|
129
|
+
const existing = await c
|
|
130
|
+
.request("GET", `/registered-apps/${app.id}/token`)
|
|
131
|
+
.then((r) => r.token)
|
|
132
|
+
.catch(() => null);
|
|
133
|
+
const { token } = await c.request("POST", `/registered-apps/${app.id}/token`);
|
|
134
|
+
const env = {
|
|
135
|
+
COE_URL: c.server,
|
|
136
|
+
COE_APP_ID: app.id,
|
|
137
|
+
COE_APP_TOKEN: token,
|
|
138
|
+
};
|
|
139
|
+
emit({ appId: app.id, token, env, rotated: Boolean(existing) }, () => {
|
|
140
|
+
if (existing) {
|
|
141
|
+
info(`Rotated ${app.id}'s token — the previous one no longer works.`);
|
|
142
|
+
}
|
|
143
|
+
info("Shown once. Add these to the app's environment:");
|
|
144
|
+
info("");
|
|
145
|
+
for (const [k, v] of Object.entries(env))
|
|
146
|
+
info(` ${k}=${v}`);
|
|
147
|
+
info("");
|
|
148
|
+
info(`Set them with: coe env set ${app.id} ${Object.entries(env).map(([k, v]) => `${k}=${v}`).join(" ")}`);
|
|
149
|
+
info("They reach the VM on the next deploy, and only if a pipeline has an env step.");
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Turns the facts into an ordered list of things to actually do.
|
|
156
|
+
*
|
|
157
|
+
* Ordered by dependency, not severity: telling someone to redeploy before they
|
|
158
|
+
* have a token to deploy is noise. An agent reads this top-down and stops
|
|
159
|
+
* having to infer the sequence from a pile of booleans.
|
|
160
|
+
*/
|
|
161
|
+
function nextSteps(f) {
|
|
162
|
+
const steps = [];
|
|
163
|
+
if (f.catalogOnly) {
|
|
164
|
+
steps.push("This is a catalogue entry: it has a dev team but no staff access, and cannot enforce " +
|
|
165
|
+
"or report anything. Integrate it with @uic-coe-connect/auth first, then change its " +
|
|
166
|
+
"integration setting in the portal.");
|
|
167
|
+
return steps;
|
|
168
|
+
}
|
|
169
|
+
if (!f.hasToken)
|
|
170
|
+
steps.push("Generate a service token: coe token generate <app>");
|
|
171
|
+
if (f.missing.length > 0) {
|
|
172
|
+
steps.push(`Set the missing environment variables: ${f.missing.join(", ")}`);
|
|
173
|
+
}
|
|
174
|
+
if (f.legacy.length > 0) {
|
|
175
|
+
steps.push(`Replace the legacy fleet-wide credentials (${f.legacy.join(", ")}) with COE_APP_TOKEN. ` +
|
|
176
|
+
"They still work, but they authenticate against every app at once.");
|
|
177
|
+
}
|
|
178
|
+
if (!f.hasPipeline) {
|
|
179
|
+
steps.push("Create a deploy pipeline: coe pipelines create <app> ...");
|
|
180
|
+
}
|
|
181
|
+
else if (!f.writesEnv) {
|
|
182
|
+
steps.push("Add an env step to the pipeline, or stored variables never reach the VM: " +
|
|
183
|
+
"coe pipelines create <app> --name Deploy pull scan env:backend ...");
|
|
184
|
+
}
|
|
185
|
+
if (f.hasToken && !f.tokenUsed) {
|
|
186
|
+
steps.push("The token has never been used, so the app has not called COEConnect. Deploy it, then " +
|
|
187
|
+
"check the app's startup log for the [coe-auth] lines.");
|
|
188
|
+
}
|
|
189
|
+
else if (!f.reporting) {
|
|
190
|
+
steps.push("No logins reported yet. Sign in to the app once, then re-check.");
|
|
191
|
+
}
|
|
192
|
+
return steps;
|
|
193
|
+
}
|
package/dist/commands/reports.js
CHANGED
|
@@ -200,6 +200,30 @@ export function registerReportCommands() {
|
|
|
200
200
|
const { report } = await client().request("POST", `/reports/${id}/comments`, { body: args.flag("note") ?? "", status });
|
|
201
201
|
emit({ report }, () => info(`✓ ${report.id} is now ${report.status}`));
|
|
202
202
|
},
|
|
203
|
+
}, {
|
|
204
|
+
name: "reports close",
|
|
205
|
+
summary: "Close a report as done, or as won't do",
|
|
206
|
+
usage: "reports close <id> [--wont-do] [--note <message>]",
|
|
207
|
+
details: [
|
|
208
|
+
" --wont-do Close as declined rather than done",
|
|
209
|
+
" --note A message to send with it — closing in silence is how you",
|
|
210
|
+
" teach people to stop filing reports",
|
|
211
|
+
],
|
|
212
|
+
async run({ args, client }) {
|
|
213
|
+
const id = args.arg(0, "id");
|
|
214
|
+
const status = args.bool("wont-do") ? "declined" : "done";
|
|
215
|
+
const { report } = await client().request("POST", `/reports/${id}/comments`, { body: args.flag("note") ?? "", status });
|
|
216
|
+
emit({ report }, () => info(`✓ Closed ${report.id} as ${report.status}`));
|
|
217
|
+
},
|
|
218
|
+
}, {
|
|
219
|
+
name: "reports reopen",
|
|
220
|
+
summary: "Reopen a closed report",
|
|
221
|
+
usage: "reports reopen <id> [--note <message>]",
|
|
222
|
+
async run({ args, client }) {
|
|
223
|
+
const id = args.arg(0, "id");
|
|
224
|
+
const { report } = await client().request("POST", `/reports/${id}/comments`, { body: args.flag("note") ?? "", status: "open" });
|
|
225
|
+
emit({ report }, () => info(`✓ Reopened ${report.id}`));
|
|
226
|
+
},
|
|
203
227
|
}, {
|
|
204
228
|
name: "reports attach",
|
|
205
229
|
summary: "Attach a file to a report",
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import { registerAppCommands } from "./commands/apps.js";
|
|
|
5
5
|
import { registerAuthCommands } from "./commands/auth.js";
|
|
6
6
|
import { registerDeployCommands } from "./commands/deploy.js";
|
|
7
7
|
import { registerEnvCommands } from "./commands/env.js";
|
|
8
|
+
import { registerEventCommands } from "./commands/events.js";
|
|
9
|
+
import { registerIntegrationCommands } from "./commands/integration.js";
|
|
8
10
|
import { registerLogCommands } from "./commands/logs.js";
|
|
9
11
|
import { registerPipelineCommands } from "./commands/pipelines.js";
|
|
10
12
|
import { registerReportCommands } from "./commands/reports.js";
|
|
@@ -17,6 +19,8 @@ registerAppCommands();
|
|
|
17
19
|
registerPipelineCommands();
|
|
18
20
|
registerDeployCommands();
|
|
19
21
|
registerAccessCommands();
|
|
22
|
+
registerIntegrationCommands();
|
|
23
|
+
registerEventCommands();
|
|
20
24
|
registerEnvCommands();
|
|
21
25
|
registerResourceCommands();
|
|
22
26
|
registerLogCommands();
|
package/dist/types.d.ts
CHANGED
|
@@ -64,6 +64,10 @@ export interface AppAccess {
|
|
|
64
64
|
export interface RegisteredApp {
|
|
65
65
|
id: string;
|
|
66
66
|
name: string;
|
|
67
|
+
/** One line shown under the app's name in the launcher. Required on write. */
|
|
68
|
+
description: string;
|
|
69
|
+
/** "dev" hides the app from everyone but its dev team and webadmins. */
|
|
70
|
+
stage?: "production" | "dev";
|
|
67
71
|
url: string;
|
|
68
72
|
/** Which VM hosts this app — deploys, logs and env all act on that machine. */
|
|
69
73
|
vmId: string;
|
|
@@ -77,6 +81,12 @@ export interface RegisteredApp {
|
|
|
77
81
|
envKeys: string[];
|
|
78
82
|
pipelines?: NamedPipeline[];
|
|
79
83
|
access?: AppAccess;
|
|
84
|
+
/**
|
|
85
|
+
* Whether the app actually calls COEConnect, or is only listed in the
|
|
86
|
+
* portal's inventory. A catalogue entry has a dev team but can't enforce
|
|
87
|
+
* staff access or report usage. Absent means "coeconnect".
|
|
88
|
+
*/
|
|
89
|
+
integration?: "coeconnect" | "catalog";
|
|
80
90
|
createdAt: string;
|
|
81
91
|
updatedAt: string;
|
|
82
92
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uic-coe-connect/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The coe CLI
|
|
3
|
+
"version": "0.10.0",
|
|
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": {
|
|
7
7
|
"coe": "./dist/index.js"
|