@ekanos/cli 0.15.0 → 0.17.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/README.md +103 -8
- package/dist/auth/ekanos-api.d.ts +23 -0
- package/dist/auth/ekanos-api.js +33 -6
- package/dist/auth/ekanos-api.js.map +1 -1
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +118 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/agents.js +3 -2
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/apps.d.ts +11 -3
- package/dist/commands/apps.js +86 -28
- package/dist/commands/apps.js.map +1 -1
- package/dist/commands/audiences.d.ts +31 -0
- package/dist/commands/audiences.js +239 -0
- package/dist/commands/audiences.js.map +1 -0
- package/dist/commands/build.d.ts +2 -0
- package/dist/commands/build.js +41 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/catalog.d.ts +4 -2
- package/dist/commands/catalog.js +166 -35
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/check.d.ts +2 -0
- package/dist/commands/check.js +43 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/codegen.d.ts +2 -0
- package/dist/commands/codegen.js +49 -0
- package/dist/commands/codegen.js.map +1 -0
- package/dist/commands/contract.d.ts +52 -0
- package/dist/commands/contract.js +300 -0
- package/dist/commands/contract.js.map +1 -0
- package/dist/commands/dev.d.ts +7 -0
- package/dist/commands/dev.js +186 -12
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/index-experience.d.ts +31 -0
- package/dist/commands/index-experience.js +21 -0
- package/dist/commands/index-experience.js.map +1 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +12 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +17 -0
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/new.d.ts +2 -0
- package/dist/commands/new.js +39 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/publish.d.ts +59 -0
- package/dist/commands/publish.js +118 -65
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/runs.d.ts +7 -3
- package/dist/commands/runs.js +21 -5
- package/dist/commands/runs.js.map +1 -1
- package/dist/commands/scope-notice.d.ts +13 -0
- package/dist/commands/scope-notice.js +12 -4
- package/dist/commands/scope-notice.js.map +1 -1
- package/dist/commands/sources-roles.d.ts +5 -0
- package/dist/commands/sources-roles.js +235 -0
- package/dist/commands/sources-roles.js.map +1 -0
- package/dist/commands/sources.d.ts +5 -0
- package/dist/commands/sources.js +8 -1
- package/dist/commands/sources.js.map +1 -1
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +150 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/team-agent-readiness.d.ts +18 -2
- package/dist/commands/team-agent-readiness.js +39 -39
- package/dist/commands/team-agent-readiness.js.map +1 -1
- package/dist/commands/team-agents-agents.d.ts +5 -2
- package/dist/commands/team-agents-agents.js +152 -15
- package/dist/commands/team-agents-agents.js.map +1 -1
- package/dist/commands/team-agents-define.d.ts +31 -0
- package/dist/commands/team-agents-define.js +176 -0
- package/dist/commands/team-agents-define.js.map +1 -0
- package/dist/commands/team-agents-mcp.js +112 -10
- package/dist/commands/team-agents-mcp.js.map +1 -1
- package/dist/commands/team-agents-triggers.d.ts +1 -1
- package/dist/commands/team-agents-triggers.js +137 -5
- package/dist/commands/team-agents-triggers.js.map +1 -1
- package/dist/commands/team-agents.d.ts +4 -1
- package/dist/commands/team-agents.js +17 -4
- package/dist/commands/team-agents.js.map +1 -1
- package/dist/commands/team-llm-key.d.ts +14 -0
- package/dist/commands/team-llm-key.js +122 -0
- package/dist/commands/team-llm-key.js.map +1 -0
- package/dist/commands/teams-audience.d.ts +12 -0
- package/dist/commands/teams-audience.js +144 -0
- package/dist/commands/teams-audience.js.map +1 -0
- package/dist/commands/teams-connect.js +123 -31
- package/dist/commands/teams-connect.js.map +1 -1
- package/dist/commands/teams-lifecycle.d.ts +90 -0
- package/dist/commands/teams-lifecycle.js +457 -0
- package/dist/commands/teams-lifecycle.js.map +1 -0
- package/dist/commands/teams-onboard.d.ts +14 -0
- package/dist/commands/teams-onboard.js +62 -13
- package/dist/commands/teams-onboard.js.map +1 -1
- package/dist/commands/teams-overview.js +6 -2
- package/dist/commands/teams-overview.js.map +1 -1
- package/dist/commands/teams-scope.js +4 -0
- package/dist/commands/teams-scope.js.map +1 -1
- package/dist/commands/teams.d.ts +25 -5
- package/dist/commands/teams.js +106 -39
- package/dist/commands/teams.js.map +1 -1
- package/dist/commands/workflows.d.ts +37 -0
- package/dist/commands/workflows.js +589 -0
- package/dist/commands/workflows.js.map +1 -0
- package/dist/compatibility.d.ts +10 -1
- package/dist/compatibility.js +10 -1
- package/dist/compatibility.js.map +1 -1
- package/dist/connect-fields.d.ts +22 -0
- package/dist/connect-fields.js +115 -0
- package/dist/connect-fields.js.map +1 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +32 -9
- package/dist/context.js.map +1 -1
- package/dist/dev-experience.d.ts +125 -0
- package/dist/dev-experience.js +128 -0
- package/dist/dev-experience.js.map +1 -0
- package/dist/experience/boundary.d.ts +12 -0
- package/dist/experience/boundary.js +260 -0
- package/dist/experience/boundary.js.map +1 -0
- package/dist/experience/build.d.ts +16 -0
- package/dist/experience/build.js +90 -0
- package/dist/experience/build.js.map +1 -0
- package/dist/experience/checks.d.ts +29 -0
- package/dist/experience/checks.js +133 -0
- package/dist/experience/checks.js.map +1 -0
- package/dist/experience/contract-sources.d.ts +36 -0
- package/dist/experience/contract-sources.js +111 -0
- package/dist/experience/contract-sources.js.map +1 -0
- package/dist/experience/definition-checks.d.ts +26 -0
- package/dist/experience/definition-checks.js +201 -0
- package/dist/experience/definition-checks.js.map +1 -0
- package/dist/experience/definition-shape.d.ts +60 -0
- package/dist/experience/definition-shape.js +2 -0
- package/dist/experience/definition-shape.js.map +1 -0
- package/dist/experience/init.d.ts +10 -0
- package/dist/experience/init.js +64 -0
- package/dist/experience/init.js.map +1 -0
- package/dist/experience/integrations-codegen.d.ts +17 -0
- package/dist/experience/integrations-codegen.js +51 -0
- package/dist/experience/integrations-codegen.js.map +1 -0
- package/dist/experience/json-schema-to-ts.d.ts +1 -0
- package/dist/experience/json-schema-to-ts.js +90 -0
- package/dist/experience/json-schema-to-ts.js.map +1 -0
- package/dist/experience/load-experience.d.ts +23 -0
- package/dist/experience/load-experience.js +115 -0
- package/dist/experience/load-experience.js.map +1 -0
- package/dist/experience/overrides.d.ts +8 -0
- package/dist/experience/overrides.js +21 -0
- package/dist/experience/overrides.js.map +1 -0
- package/dist/experience/pin-integration.d.ts +8 -0
- package/dist/experience/pin-integration.js +17 -0
- package/dist/experience/pin-integration.js.map +1 -0
- package/dist/experience/pin-ops.d.ts +12 -0
- package/dist/experience/pin-ops.js +36 -0
- package/dist/experience/pin-ops.js.map +1 -0
- package/dist/experience/pins.d.ts +35 -0
- package/dist/experience/pins.js +91 -0
- package/dist/experience/pins.js.map +1 -0
- package/dist/experience/project.d.ts +52 -0
- package/dist/experience/project.js +149 -0
- package/dist/experience/project.js.map +1 -0
- package/dist/experience/publish.d.ts +51 -0
- package/dist/experience/publish.js +144 -0
- package/dist/experience/publish.js.map +1 -0
- package/dist/experience/regenerate.d.ts +10 -0
- package/dist/experience/regenerate.js +33 -0
- package/dist/experience/regenerate.js.map +1 -0
- package/dist/experience/report.d.ts +9 -0
- package/dist/experience/report.js +23 -0
- package/dist/experience/report.js.map +1 -0
- package/dist/harness-scaffold.d.ts +52 -18
- package/dist/harness-scaffold.js +189 -9
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +494 -127
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts +31 -0
- package/dist/invocation.js +58 -0
- package/dist/invocation.js.map +1 -0
- package/dist/project-checks.d.ts +12 -0
- package/dist/project-checks.js +46 -0
- package/dist/project-checks.js.map +1 -1
- package/dist/project.d.ts +14 -0
- package/dist/project.js +10 -0
- package/dist/project.js.map +1 -1
- package/dist/repo/publish.d.ts +42 -0
- package/dist/repo/publish.js +225 -0
- package/dist/repo/publish.js.map +1 -0
- package/dist/repo/scaffold.d.ts +30 -0
- package/dist/repo/scaffold.js +165 -0
- package/dist/repo/scaffold.js.map +1 -0
- package/dist/runs-api.js +8 -3
- package/dist/runs-api.js.map +1 -1
- package/dist/safe-fs.d.ts +18 -0
- package/dist/safe-fs.js +57 -0
- package/dist/safe-fs.js.map +1 -1
- package/dist/secret-form.d.ts +48 -0
- package/dist/secret-form.js +212 -0
- package/dist/secret-form.js.map +1 -0
- package/dist/secret-input.d.ts +96 -0
- package/dist/secret-input.js +299 -0
- package/dist/secret-input.js.map +1 -0
- package/dist/team-connect-api.d.ts +78 -3
- package/dist/team-connect-api.js +17 -2
- package/dist/team-connect-api.js.map +1 -1
- package/dist/team-lifecycle-api.d.ts +435 -0
- package/dist/team-lifecycle-api.js +301 -0
- package/dist/team-lifecycle-api.js.map +1 -0
- package/dist/teams-api.d.ts +49 -1
- package/dist/teams-api.js +149 -27
- package/dist/teams-api.js.map +1 -1
- package/dist/unknown-command.d.ts +18 -0
- package/dist/unknown-command.js +50 -0
- package/dist/unknown-command.js.map +1 -0
- package/dist/validate-findings.js +2 -1
- package/dist/validate-findings.js.map +1 -1
- package/dist/workflows-api.d.ts +69 -0
- package/dist/workflows-api.js +182 -0
- package/dist/workflows-api.js.map +1 -0
- package/package.json +3 -3
- package/templates/AGENTS.md.tmpl +6 -4
- package/templates/claude-skill.md.tmpl +4 -1
- package/templates/ekanos.json.tmpl +1 -0
- package/templates/experience/AGENTS.md.tmpl +22 -0
- package/templates/experience/ekanos.json.tmpl +7 -0
- package/templates/experience/ekanos.lock.json.tmpl +4 -0
- package/templates/experience/eslint.config.mjs.tmpl +25 -0
- package/templates/experience/gitignore.tmpl +17 -0
- package/templates/experience/package.json.tmpl +41 -0
- package/templates/experience/src/.ekanos/integrations.d.ts.tmpl +3 -0
- package/templates/experience/src/experience.ts.tmpl +30 -0
- package/templates/experience/src/widgets/hello-widget.tsx.tmpl +40 -0
- package/templates/experience/tsconfig.json.tmpl +20 -0
- package/templates/harness/app/%5F%5Fekanos/api/[...path]/route.ts.tmpl +9 -0
- package/templates/harness/app/[slug]/activation/page.tsx.tmpl +1 -1
- package/templates/harness/app/[slug]/page.tsx.tmpl +1 -1
- package/templates/harness/app/[slug]/tile/page.tsx.tmpl +1 -1
- package/templates/harness/app/[slug]/triggers/page.tsx.tmpl +1 -1
- package/templates/harness/app/[slug]/widgets/[widgetId]/page.tsx.tmpl +1 -1
- package/templates/harness/app/[slug]/widgets/page.tsx.tmpl +1 -1
- package/templates/harness/app/experience/layout.tsx.tmpl +17 -0
- package/templates/harness/app/experience/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/pages/[id]/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/review-queue/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/runs/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/widgets/[id]/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/widgets/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/workflows/[id]/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/workflows/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/connect/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/events/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/health/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/operations/[kind]/[name]/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/operations/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/activation/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/layout.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/tile/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/triggers/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/widgets/[widgetId]/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/widgets/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/runs/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/layout.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/page.tsx.tmpl +1 -0
- package/templates/harness/app/page.experience.tsx.tmpl +5 -0
- package/templates/harness/repo/app/%5F%5Fekanos/api/[...path]/route.ts.tmpl +11 -0
- package/templates/harness/repo/app/(repo)/connections/page.tsx.tmpl +1 -0
- package/templates/harness/repo/app/(repo)/layout.tsx.tmpl +1 -0
- package/templates/harness/repo/app/(repo)/page.tsx.tmpl +1 -0
- package/templates/harness/repo/app/experiences/[name]/connect/page.tsx.tmpl +1 -0
- package/templates/harness/repo/app/experiences/[name]/layout.tsx.tmpl +9 -0
- package/templates/harness/repo/app/experiences/page.tsx.tmpl +5 -0
- package/templates/harness/repo/app/harness-shell.tsx.tmpl +9 -0
- package/templates/harness/repo/app/integrations/[slug]/layout.tsx.tmpl +1 -0
- package/templates/harness/repo/app/integrations/page.tsx.tmpl +5 -0
- package/templates/harness/repo/app/repo-experiences.ts.tmpl +5 -0
- package/templates/harness/styles/globals.css.tmpl +15 -1
- package/templates/package.json.tmpl +4 -4
- package/templates/skills/build-agent/SKILL.md +43 -10
- package/templates/skills/build-agent/references/agent-json.md +4 -1
- package/templates/skills/build-agent/references/app-setup.md +14 -7
- package/templates/skills/build-agent/references/rollout.md +26 -2
- package/templates/skills/build-agent/references/set-up-an-integration.md +46 -29
- package/templates/skills/build-integration/references/widget-design.md +1 -1
- package/templates/skills/ekanos-platform/SKILL.md +66 -14
- package/templates/skills/polish-widgets/references/patterns.md +3 -3
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { requireSession, resolveAuthEnvironment } from '../auth/session.js';
|
|
2
|
+
import { invalidStateError, usageError } from '../errors.js';
|
|
3
|
+
import { resolveSourceTarget } from '../source-target.js';
|
|
4
|
+
import { listAudiences, setMemberAudience, setTeamAudience, showMemberAudience, showTeamAudience, } from '../teams-api.js';
|
|
5
|
+
import { TeamsClient } from './teams.js';
|
|
6
|
+
const USAGE = 'Run "ekanos teams audience <team>" to read a team’s audience, "ekanos teams audience <team> <audience> [--yes]" to set it, or add --member <email> (with <audience>, or --inherit to clear it) for one person’s own.';
|
|
7
|
+
/**
|
|
8
|
+
* `teams audience <team> [<audience>] [--member <email> [--inherit]]` — the
|
|
9
|
+
* audience a team starts its members on, and a member's own override of it
|
|
10
|
+
* (a Source admin's only; audited). Without an audience it reads. Moving
|
|
11
|
+
* people to an audience that shows fewer products hides the difference from
|
|
12
|
+
* them in the app, so that needs `--yes`. `teams scope` is the deprecated
|
|
13
|
+
* form of this.
|
|
14
|
+
*/
|
|
15
|
+
export async function runTeamAudience(ctx, args, rest) {
|
|
16
|
+
var _a, _b;
|
|
17
|
+
const [rawTeam, audience, ...extra] = rest;
|
|
18
|
+
const team = rawTeam === null || rawTeam === void 0 ? void 0 : rawTeam.trim().toLowerCase();
|
|
19
|
+
if (!team || !/^[a-z0-9][a-z0-9-]*$/.test(team)) {
|
|
20
|
+
throw usageError(rawTeam
|
|
21
|
+
? `"${rawTeam}" is not a team slug.`
|
|
22
|
+
: 'teams audience needs a team.', USAGE);
|
|
23
|
+
}
|
|
24
|
+
if (extra.length > 0) {
|
|
25
|
+
throw usageError(`Unexpected argument "${extra[0]}".`, USAGE);
|
|
26
|
+
}
|
|
27
|
+
const member = typeof ((_a = args.flags) === null || _a === void 0 ? void 0 : _a.member) === 'string'
|
|
28
|
+
? args.flags.member.trim().toLowerCase()
|
|
29
|
+
: undefined;
|
|
30
|
+
const inherit = ((_b = args.flags) === null || _b === void 0 ? void 0 : _b.inherit) === true;
|
|
31
|
+
if (inherit && !member) {
|
|
32
|
+
throw usageError('--inherit clears one member’s audience.', USAGE);
|
|
33
|
+
}
|
|
34
|
+
if (inherit && audience) {
|
|
35
|
+
throw usageError('Name an audience or pass --inherit, not both.', 'ekanos teams audience <team> --member <email> --inherit puts them back on the team’s.');
|
|
36
|
+
}
|
|
37
|
+
if (member && !/^[^@\s]+@[^@\s]+$/.test(member)) {
|
|
38
|
+
throw usageError(`"${member}" is not an email address.`, USAGE);
|
|
39
|
+
}
|
|
40
|
+
const env = resolveAuthEnvironment(ctx, args.host, args.env, {
|
|
41
|
+
projectDir: args.dir,
|
|
42
|
+
});
|
|
43
|
+
const resolved = await resolveSourceTarget(env, requireSession(env), args);
|
|
44
|
+
const client = new TeamsClient(env, resolved.session);
|
|
45
|
+
const source = resolved.source;
|
|
46
|
+
const target = { source, team };
|
|
47
|
+
if (member) {
|
|
48
|
+
const person = Object.assign(Object.assign({}, target), { email: member });
|
|
49
|
+
const current = await client.call((token) => showMemberAudience(env.host, token, person));
|
|
50
|
+
if (!audience && !inherit) {
|
|
51
|
+
return ctx.succeed(Object.assign({ host: env.host }, current), describeMember(ctx, current));
|
|
52
|
+
}
|
|
53
|
+
const next = inherit
|
|
54
|
+
? (await client.call((token) => showTeamAudience(env.host, token, target))).audience.id
|
|
55
|
+
: audience;
|
|
56
|
+
await requireYesIfNarrowing(client, env.host, {
|
|
57
|
+
source,
|
|
58
|
+
from: current.audience,
|
|
59
|
+
to: next,
|
|
60
|
+
who: `${member}`,
|
|
61
|
+
yes: args.yes,
|
|
62
|
+
retry: `ekanos teams audience ${team} ${inherit ? '' : `${quote(audience)} `}--member ${member}${inherit ? ' --inherit' : ''} --source ${source} --yes`,
|
|
63
|
+
});
|
|
64
|
+
const result = await client.call((token) => setMemberAudience(env.host, token, person, inherit ? null : audience));
|
|
65
|
+
return ctx.succeed(Object.assign({ host: env.host }, result), describeMember(ctx, result, result.before));
|
|
66
|
+
}
|
|
67
|
+
const current = await client.call((token) => showTeamAudience(env.host, token, target));
|
|
68
|
+
if (!audience) {
|
|
69
|
+
return ctx.succeed(Object.assign({ host: env.host }, current), describeTeam(ctx, current));
|
|
70
|
+
}
|
|
71
|
+
await requireYesIfNarrowing(client, env.host, {
|
|
72
|
+
source,
|
|
73
|
+
from: current.audience,
|
|
74
|
+
to: audience,
|
|
75
|
+
who: `members of ${team} on the team default`,
|
|
76
|
+
yes: args.yes,
|
|
77
|
+
retry: `ekanos teams audience ${team} ${quote(audience)} --source ${source} --yes`,
|
|
78
|
+
});
|
|
79
|
+
const result = await client.call((token) => setTeamAudience(env.host, token, target, audience));
|
|
80
|
+
return ctx.succeed(Object.assign({ host: env.host }, result), describeTeam(ctx, result, result.before));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Refuses, before any write, a move to an audience that shows less than the
|
|
84
|
+
* one people are on, unless `--yes`. An audience that shows every entitled
|
|
85
|
+
* product can only be left for a narrower one, which is also refused.
|
|
86
|
+
*/
|
|
87
|
+
async function requireYesIfNarrowing(client, host, input) {
|
|
88
|
+
if (input.yes)
|
|
89
|
+
return;
|
|
90
|
+
const { audiences } = await client.call((token) => listAudiences(host, token, input.source));
|
|
91
|
+
const from = audiences.find((entry) => entry.id === input.from.id);
|
|
92
|
+
const to = findAudience(audiences, input.to);
|
|
93
|
+
if (!from || !to || to.includesAllEntitled)
|
|
94
|
+
return;
|
|
95
|
+
const kept = new Set(to.products.map((product) => product.slug));
|
|
96
|
+
const lost = from.includesAllEntitled
|
|
97
|
+
? null
|
|
98
|
+
: from.products.filter((product) => !kept.has(product.slug));
|
|
99
|
+
if (lost !== null && lost.length === 0)
|
|
100
|
+
return;
|
|
101
|
+
throw invalidStateError(lost === null
|
|
102
|
+
? `${to.name} does not show every product the Source has, as ${from.name} does: ${input.who} may stop seeing some in the app.`
|
|
103
|
+
: `${input.who} would stop seeing ${lost.map((product) => product.slug).join(', ')} in the app: ${from.name} shows ${lost.length === 1 ? 'it' : 'them'}, ${to.name} does not.`, `Re-run with --yes: ${input.retry}`);
|
|
104
|
+
}
|
|
105
|
+
function findAudience(audiences, ref) {
|
|
106
|
+
const wanted = ref.trim().toLowerCase();
|
|
107
|
+
return audiences.find((entry) => entry.id === wanted || entry.name.toLowerCase() === wanted);
|
|
108
|
+
}
|
|
109
|
+
function describeTeam(ctx, result, before) {
|
|
110
|
+
var _a;
|
|
111
|
+
return ctx.ui.result({
|
|
112
|
+
title: `${result.team.slug} starts its members on ${result.audience.name}`,
|
|
113
|
+
rows: [
|
|
114
|
+
['Audience', result.audience.name],
|
|
115
|
+
...(before !== undefined
|
|
116
|
+
? [['Before', (_a = before === null || before === void 0 ? void 0 : before.name) !== null && _a !== void 0 ? _a : 'none']]
|
|
117
|
+
: []),
|
|
118
|
+
],
|
|
119
|
+
notes: [
|
|
120
|
+
'A member’s own audience overrides it: "ekanos teams audience <team> <audience> --member <email>".',
|
|
121
|
+
'"ekanos audiences show <audience>" lists what it shows.',
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function describeMember(ctx, result, before) {
|
|
126
|
+
var _a;
|
|
127
|
+
return ctx.ui.result({
|
|
128
|
+
title: `${result.member.email} is on ${result.audience.name}`,
|
|
129
|
+
rows: [
|
|
130
|
+
['Audience', result.audience.name],
|
|
131
|
+
[
|
|
132
|
+
'From',
|
|
133
|
+
result.isOverride ? 'their own' : `the ${result.team.slug} default`,
|
|
134
|
+
],
|
|
135
|
+
...(before !== undefined
|
|
136
|
+
? [['Before', (_a = before === null || before === void 0 ? void 0 : before.name) !== null && _a !== void 0 ? _a : 'none']]
|
|
137
|
+
: []),
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function quote(name) {
|
|
142
|
+
return /^[A-Za-z0-9._-]+$/.test(name) ? name : `"${name}"`;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=teams-audience.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teams-audience.js","sourceRoot":"","sources":["../../src/commands/teams-audience.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n Audience,\n AudienceRef,\n MemberAudience,\n TeamAudience,\n} from '@ekanos/integration-schema';\n\nimport { requireSession, resolveAuthEnvironment } from '../auth/session';\nimport type { CliContext } from '../context';\nimport { invalidStateError, usageError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { resolveSourceTarget } from '../source-target';\nimport {\n listAudiences,\n setMemberAudience,\n setTeamAudience,\n showMemberAudience,\n showTeamAudience,\n} from '../teams-api';\nimport { TeamsClient, type TeamsArgs } from './teams';\n\nconst USAGE =\n 'Run \"ekanos teams audience <team>\" to read a team’s audience, \"ekanos teams audience <team> <audience> [--yes]\" to set it, or add --member <email> (with <audience>, or --inherit to clear it) for one person’s own.';\n\n/**\n * `teams audience <team> [<audience>] [--member <email> [--inherit]]` — the\n * audience a team starts its members on, and a member's own override of it\n * (a Source admin's only; audited). Without an audience it reads. Moving\n * people to an audience that shows fewer products hides the difference from\n * them in the app, so that needs `--yes`. `teams scope` is the deprecated\n * form of this.\n */\nexport async function runTeamAudience(\n ctx: CliContext,\n args: TeamsArgs,\n rest: readonly string[],\n): Promise<ExitCode> {\n const [rawTeam, audience, ...extra] = rest;\n const team = rawTeam?.trim().toLowerCase();\n if (!team || !/^[a-z0-9][a-z0-9-]*$/.test(team)) {\n throw usageError(\n rawTeam\n ? `\"${rawTeam}\" is not a team slug.`\n : 'teams audience needs a team.',\n USAGE,\n );\n }\n if (extra.length > 0) {\n throw usageError(`Unexpected argument \"${extra[0]}\".`, USAGE);\n }\n const member =\n typeof args.flags?.member === 'string'\n ? args.flags.member.trim().toLowerCase()\n : undefined;\n const inherit = args.flags?.inherit === true;\n if (inherit && !member) {\n throw usageError('--inherit clears one member’s audience.', USAGE);\n }\n if (inherit && audience) {\n throw usageError(\n 'Name an audience or pass --inherit, not both.',\n 'ekanos teams audience <team> --member <email> --inherit puts them back on the team’s.',\n );\n }\n if (member && !/^[^@\\s]+@[^@\\s]+$/.test(member)) {\n throw usageError(`\"${member}\" is not an email address.`, USAGE);\n }\n\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: args.dir,\n });\n const resolved = await resolveSourceTarget(env, requireSession(env), args);\n const client = new TeamsClient(env, resolved.session);\n const source = resolved.source;\n const target = { source, team };\n\n if (member) {\n const person = { ...target, email: member };\n const current = await client.call((token) =>\n showMemberAudience(env.host, token, person),\n );\n if (!audience && !inherit) {\n return ctx.succeed(\n { host: env.host, ...current },\n describeMember(ctx, current),\n );\n }\n\n const next = inherit\n ? (\n await client.call((token) =>\n showTeamAudience(env.host, token, target),\n )\n ).audience.id\n : audience!;\n await requireYesIfNarrowing(client, env.host, {\n source,\n from: current.audience,\n to: next,\n who: `${member}`,\n yes: args.yes,\n retry: `ekanos teams audience ${team} ${inherit ? '' : `${quote(audience!)} `}--member ${member}${inherit ? ' --inherit' : ''} --source ${source} --yes`,\n });\n const result = await client.call((token) =>\n setMemberAudience(env.host, token, person, inherit ? null : audience!),\n );\n return ctx.succeed(\n { host: env.host, ...result },\n describeMember(ctx, result, result.before),\n );\n }\n\n const current = await client.call((token) =>\n showTeamAudience(env.host, token, target),\n );\n if (!audience) {\n return ctx.succeed(\n { host: env.host, ...current },\n describeTeam(ctx, current),\n );\n }\n await requireYesIfNarrowing(client, env.host, {\n source,\n from: current.audience,\n to: audience,\n who: `members of ${team} on the team default`,\n yes: args.yes,\n retry: `ekanos teams audience ${team} ${quote(audience)} --source ${source} --yes`,\n });\n const result = await client.call((token) =>\n setTeamAudience(env.host, token, target, audience),\n );\n return ctx.succeed(\n { host: env.host, ...result },\n describeTeam(ctx, result, result.before),\n );\n}\n\n/**\n * Refuses, before any write, a move to an audience that shows less than the\n * one people are on, unless `--yes`. An audience that shows every entitled\n * product can only be left for a narrower one, which is also refused.\n */\nasync function requireYesIfNarrowing(\n client: TeamsClient,\n host: string,\n input: {\n source: string;\n from: AudienceRef;\n to: string;\n who: string;\n yes: boolean;\n retry: string;\n },\n): Promise<void> {\n if (input.yes) return;\n const { audiences } = await client.call((token) =>\n listAudiences(host, token, input.source),\n );\n const from = audiences.find((entry) => entry.id === input.from.id);\n const to = findAudience(audiences, input.to);\n if (!from || !to || to.includesAllEntitled) return;\n\n const kept = new Set(to.products.map((product) => product.slug));\n const lost = from.includesAllEntitled\n ? null\n : from.products.filter((product) => !kept.has(product.slug));\n if (lost !== null && lost.length === 0) return;\n\n throw invalidStateError(\n lost === null\n ? `${to.name} does not show every product the Source has, as ${from.name} does: ${input.who} may stop seeing some in the app.`\n : `${input.who} would stop seeing ${lost.map((product) => product.slug).join(', ')} in the app: ${from.name} shows ${lost.length === 1 ? 'it' : 'them'}, ${to.name} does not.`,\n `Re-run with --yes: ${input.retry}`,\n );\n}\n\nfunction findAudience(\n audiences: Audience[],\n ref: string,\n): Audience | undefined {\n const wanted = ref.trim().toLowerCase();\n return audiences.find(\n (entry) => entry.id === wanted || entry.name.toLowerCase() === wanted,\n );\n}\n\nfunction describeTeam(\n ctx: CliContext,\n result: TeamAudience,\n before?: AudienceRef | null,\n): string {\n return ctx.ui.result({\n title: `${result.team.slug} starts its members on ${result.audience.name}`,\n rows: [\n ['Audience', result.audience.name],\n ...(before !== undefined\n ? ([['Before', before?.name ?? 'none']] as Array<[string, string]>)\n : []),\n ],\n notes: [\n 'A member’s own audience overrides it: \"ekanos teams audience <team> <audience> --member <email>\".',\n '\"ekanos audiences show <audience>\" lists what it shows.',\n ],\n });\n}\n\nfunction describeMember(\n ctx: CliContext,\n result: MemberAudience,\n before?: AudienceRef | null,\n): string {\n return ctx.ui.result({\n title: `${result.member.email} is on ${result.audience.name}`,\n rows: [\n ['Audience', result.audience.name],\n [\n 'From',\n result.isOverride ? 'their own' : `the ${result.team.slug} default`,\n ],\n ...(before !== undefined\n ? ([['Before', before?.name ?? 'none']] as Array<[string, string]>)\n : []),\n ],\n });\n}\n\nfunction quote(name: string): string {\n return /^[A-Za-z0-9._-]+$/.test(name) ? name : `\"${name}\"`;\n}\n"]}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { TeamAgentListSchema, } from '@ekanos/integration-schema';
|
|
2
|
+
import { fieldPrompt, parseFieldValue } from '../connect-fields.js';
|
|
2
3
|
import { invalidStateError, usageError, validationError } from '../errors.js';
|
|
4
|
+
import { cli } from '../invocation.js';
|
|
3
5
|
import { assertBrowserSafeUrl, openInBrowser } from '../open-url.js';
|
|
6
|
+
import { FIELD_SECRET_FORMS, collectSecrets, parsePlainValues, readSecretFlags, secretInputGiven, } from '../secret-input.js';
|
|
4
7
|
import { showTeamConnect, startTeamConnect, } from '../team-connect-api.js';
|
|
5
8
|
import { call, showTeamIntegration } from '../teams-api.js';
|
|
6
9
|
import { promptHidden, promptLine } from './apps.js';
|
|
7
10
|
import { scopeHiddenLines } from './scope-notice.js';
|
|
8
|
-
import { canPrompt, checklist, promptAndSaveTeamApp, } from './teams.js';
|
|
11
|
+
import { canPrompt, checklist, promptAndSaveTeamApp, teamsSecretIO, } from './teams.js';
|
|
9
12
|
import { isTeamChannel, runTeamChannelInstall } from './teams-channel-install.js';
|
|
10
13
|
/**
|
|
11
14
|
* `teams connect <team> <product>` — connect a product for one team, doing
|
|
@@ -161,7 +164,7 @@ async function warnOtherRunsAs(ctx, client, host, source, plan) {
|
|
|
161
164
|
item.viaDetail.toLowerCase() !== email)) === null || _d === void 0 ? void 0 : _d.viaDetail;
|
|
162
165
|
if (!runsAs)
|
|
163
166
|
continue;
|
|
164
|
-
ctx.warn(`Note: ${entry.key} runs as ${runsAs} and uses ${runsAs}'s ${plan.product.name} connection, not ${email}'s. For the agent, run:
|
|
167
|
+
ctx.warn(`Note: ${entry.key} runs as ${runsAs} and uses ${runsAs}'s ${plan.product.name} connection, not ${email}'s. For the agent, run: ${cli()} teams connect ${plan.team.slug} ${plan.product.slug} --as ${runsAs} --source ${source}`);
|
|
165
168
|
}
|
|
166
169
|
}
|
|
167
170
|
/**
|
|
@@ -192,7 +195,7 @@ async function offerChannels(ctx, args, client, host, source, input, done) {
|
|
|
192
195
|
done.summary,
|
|
193
196
|
...waiting.flatMap((entry) => [
|
|
194
197
|
`Next: ${why(entry)}`,
|
|
195
|
-
` Install it in your terminal:
|
|
198
|
+
` Install it in your terminal: ${cli()} ${command(entry.connectionId)}`,
|
|
196
199
|
]),
|
|
197
200
|
].join('\n'));
|
|
198
201
|
}
|
|
@@ -229,31 +232,14 @@ async function connectionLabel(client, host, target, plan) {
|
|
|
229
232
|
}
|
|
230
233
|
async function activate(ctx, args, client, host, target, plan) {
|
|
231
234
|
var _a;
|
|
232
|
-
var _b, _c
|
|
235
|
+
var _b, _c;
|
|
233
236
|
if (plan.scope === 'user' && !plan.member) {
|
|
234
|
-
throw invalidStateError((_b = plan.memberProblem) !== null && _b !== void 0 ? _b : `${plan.product.name} is a personal connection: each member connects their own.`,
|
|
237
|
+
throw invalidStateError((_b = plan.memberProblem) !== null && _b !== void 0 ? _b : `${plan.product.name} is a personal connection: each member connects their own.`, joinHint(plan, target));
|
|
235
238
|
}
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
ctx.log(`${plan.product.name} for ${plan.scope === 'user' ? 'you, in' : 'team'} ${plan.team.slug}: enter what the app's form asks. Secrets are hidden and never shown again.`);
|
|
242
|
-
for (const field of plan.fields) {
|
|
243
|
-
if (field.description)
|
|
244
|
-
ctx.log(` ${field.description}`);
|
|
245
|
-
const suffix = `${field.required ? '' : ' (optional — Enter skips)'}${field.sensitive ? ' (hidden)' : ''}: `;
|
|
246
|
-
const value = (await (field.sensitive
|
|
247
|
-
? ((_c = args.askSecret) !== null && _c !== void 0 ? _c : promptHidden)
|
|
248
|
-
: ((_d = args.ask) !== null && _d !== void 0 ? _d : promptLine))(`${field.label}${suffix}`)).trim();
|
|
249
|
-
if (value)
|
|
250
|
-
fields[field.name] = value;
|
|
251
|
-
else if (field.required) {
|
|
252
|
-
throw validationError(`${field.label} is needed.`, `Run "ekanos teams connect ${target.team} ${target.product}" again.`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
const started = await client.call((token) => startTeamConnect(host, token, target, { fields }));
|
|
239
|
+
const entered = plan.mode === 'fields'
|
|
240
|
+
? await enterFields(ctx, args, plan, connectAgain(target, plan))
|
|
241
|
+
: { fields: {}, set: [] };
|
|
242
|
+
const started = await client.call((token) => startTeamConnect(host, token, target, Object.assign({ fields: entered.fields }, (entered.set.length > 0 ? { set: entered.set } : {}))));
|
|
257
243
|
return ctx.succeed(Object.assign(Object.assign({ host }, started), { connected: started.plan.connected }), ctx.ui.result({
|
|
258
244
|
title: `Connected ${started.plan.product.name} for ${started.plan.team.slug}`,
|
|
259
245
|
rows: [
|
|
@@ -261,7 +247,7 @@ async function activate(ctx, args, client, host, target, plan) {
|
|
|
261
247
|
[
|
|
262
248
|
'For',
|
|
263
249
|
started.plan.scope === 'user'
|
|
264
|
-
? `you (${(
|
|
250
|
+
? `you (${(_c = (_a = started.plan.member) === null || _a === void 0 ? void 0 : _a.email) !== null && _c !== void 0 ? _c : 'your account'}) — a personal connection`
|
|
265
251
|
: 'the whole team',
|
|
266
252
|
],
|
|
267
253
|
['Team page', started.plan.connectUrl],
|
|
@@ -274,21 +260,32 @@ async function connectOAuth(ctx, args, client, host, target, plan, asEmail, opti
|
|
|
274
260
|
const name = options.name;
|
|
275
261
|
const again = `ekanos teams connect ${target.team} ${target.product}${named ? ` --connection ${plan.connectionId}` : ''}`;
|
|
276
262
|
if (!plan.member) {
|
|
277
|
-
throw invalidStateError((_a = plan.memberProblem) !== null && _a !== void 0 ? _a : 'No member of the team can sign in yet.',
|
|
263
|
+
throw invalidStateError((_a = plan.memberProblem) !== null && _a !== void 0 ? _a : 'No member of the team can sign in yet.', plan.scope === 'user'
|
|
264
|
+
? joinHint(plan, target)
|
|
265
|
+
: `Name a member of ${plan.team.slug} who signs in: ${again} --as <email>. Or join it and sign in yourself: ekanos teams members add ${plan.team.slug} --self --role admin`);
|
|
278
266
|
}
|
|
279
267
|
if (plan.teamApp) {
|
|
280
268
|
const status = await client.call((token) => showTeamIntegration(host, token, target));
|
|
281
269
|
const connectionId = (_b = plan.connectionId) !== null && _b !== void 0 ? _b : 'default';
|
|
282
270
|
const app = (_c = status.teamApps.find((entry) => entry.connectionId === connectionId)) !== null && _c !== void 0 ? _c : (named ? undefined : status.teamApps[0]);
|
|
283
271
|
if (app && app.fields.some((field) => !field.set)) {
|
|
284
|
-
if (!canPrompt(ctx, args)
|
|
285
|
-
|
|
272
|
+
if (!canPrompt(ctx, args) &&
|
|
273
|
+
!secretInputGiven(readSecretFlags(args.flags))) {
|
|
274
|
+
throw usageError(`${name} needs ${plan.team.slug}'s own app first (${app.fields
|
|
275
|
+
.filter((field) => !field.set)
|
|
276
|
+
.map((field) => field.secretName)
|
|
277
|
+
.join(', ')}), and there is no terminal to ask at.`, `Without a terminal, pass --set <field>=<value> for the plain fields and ${FIELD_SECRET_FORMS}. Or run it yourself in your terminal: ${cli()} ${again} --source ${plan.source.slug}`);
|
|
286
278
|
}
|
|
287
279
|
ctx.log(`${name} signs ${plan.team.slug} in through the team's own app. Set it up first:`);
|
|
288
280
|
await promptAndSaveTeamApp(ctx, args, client, host, target, status, app);
|
|
289
281
|
}
|
|
290
282
|
}
|
|
291
|
-
|
|
283
|
+
// An app sign-in (GitHub, ROAM) activates on return with what the app's
|
|
284
|
+
// form would add after the sign-in: asked now, applied then.
|
|
285
|
+
const entered = plan.signIn === 'app' && plan.fields.length > 0
|
|
286
|
+
? await enterFields(ctx, args, plan, `${again} --source ${plan.source.slug}`)
|
|
287
|
+
: undefined;
|
|
288
|
+
const started = await client.call((token) => startTeamConnect(host, token, target, Object.assign(Object.assign(Object.assign({}, (asEmail ? { as: asEmail } : {})), (entered ? { fields: entered.fields } : {})), (entered && entered.set.length > 0 ? { set: entered.set } : {}))));
|
|
292
289
|
if (started.result !== 'consent') {
|
|
293
290
|
throw invalidStateError(`The host answered "${started.result}" where a sign-in link was expected.`, 'Run "ekanos upgrade" — the CLI and the host disagree on the connect contract.');
|
|
294
291
|
}
|
|
@@ -303,11 +300,21 @@ async function connectOAuth(ctx, args, client, host, target, plan, asEmail, opti
|
|
|
303
300
|
: `Sign in to ${plan.product.name} as the account ${plan.team.slug} should use${plan.scope === 'user' ? ' (a personal connection: yours)' : ''}:`,
|
|
304
301
|
` ${started.authorizeUrl}`,
|
|
305
302
|
` (one-time link, valid until ${expires} UTC)`,
|
|
303
|
+
...(plan.signIn === 'app'
|
|
304
|
+
? [
|
|
305
|
+
` It opens ${plan.product.name}'s sign-in through ${plan.source.slug}'s app: if the browser asks you to sign in to the app first, do, then open the link again.`,
|
|
306
|
+
]
|
|
307
|
+
: []),
|
|
306
308
|
].join('\n')
|
|
307
309
|
: [
|
|
308
310
|
`Send this link to ${(_e = member.email) !== null && _e !== void 0 ? _e : 'the member'} — they ${named ? `install ${name}` : `sign in to ${plan.product.name}`} for ${plan.team.slug}${plan.scope === 'user' ? ' (a personal connection: theirs)' : ''}:`,
|
|
309
311
|
` ${started.authorizeUrl}`,
|
|
310
312
|
` (one-time, for them only, valid until ${expires} UTC)`,
|
|
313
|
+
...(plan.signIn === 'app'
|
|
314
|
+
? [
|
|
315
|
+
` They open it signed in to ${plan.source.slug}'s app as themselves.`,
|
|
316
|
+
]
|
|
317
|
+
: []),
|
|
311
318
|
].join('\n'));
|
|
312
319
|
// Only ever the caller's own browser: opening a link meant for another
|
|
313
320
|
// member would sign the caller's provider account in for them.
|
|
@@ -360,6 +367,91 @@ async function connectOAuth(ctx, args, client, host, target, plan, asEmail, opti
|
|
|
360
367
|
}
|
|
361
368
|
throw validationError('The sign-in did not finish before the link expired.', `Run "${again}${asEmail ? ` --as ${asEmail}` : ''}" again for a fresh link.`);
|
|
362
369
|
}
|
|
370
|
+
/** The command that connects it again, for a hint. */
|
|
371
|
+
function connectAgain(target, plan) {
|
|
372
|
+
return `ekanos teams connect ${target.team} ${target.product} --source ${plan.source.slug}`;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* A personal connection is the member's own: whoever runs connect makes
|
|
376
|
+
* theirs, so a caller who is not on the team joins it first.
|
|
377
|
+
*/
|
|
378
|
+
function joinHint(plan, target) {
|
|
379
|
+
return `It is yours to connect once you are a member: ekanos teams members add ${plan.team.slug} --self, then ${connectAgain(target, plan)}. For someone else's, send them a link: ${connectAgain(target, plan)} --as <their-email>.`;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* The activation fields, through the shared secret input (`secret-input.ts`)
|
|
383
|
+
* that every command taking a secret uses: `--set <field>=<value>` for a
|
|
384
|
+
* plain field; for a secret (which includes a JSON value, which can carry
|
|
385
|
+
* one) `--set-secret <field>=env:…|file:…|stdin`, `--secret-env|file|stdin`
|
|
386
|
+
* for a single one, or `--browser`; otherwise a prompt at a terminal, hidden
|
|
387
|
+
* for a secret. Each value is then typed by its field (`connect-fields.ts`).
|
|
388
|
+
*
|
|
389
|
+
* In a script or an agent run with no secret flags, an optional field
|
|
390
|
+
* nobody gave is skipped rather than refused, so a product whose fields are
|
|
391
|
+
* all optional (GitHub) goes straight to its sign-in.
|
|
392
|
+
*
|
|
393
|
+
* `set` names the fields given by `--set`: the host refuses a secret among
|
|
394
|
+
* them (`SECRET_AS_ARGUMENT`), as `--set` here already does.
|
|
395
|
+
*/
|
|
396
|
+
async function enterFields(ctx, args, plan, again) {
|
|
397
|
+
var _a;
|
|
398
|
+
const plain = parsePlainValues((_a = args.flags) === null || _a === void 0 ? void 0 : _a.set, 'default_labels=bug,triage');
|
|
399
|
+
const secret = readSecretFlags(args.flags);
|
|
400
|
+
const interactive = canPrompt(ctx, args);
|
|
401
|
+
const asking = interactive || secretInputGiven(secret);
|
|
402
|
+
// Without a terminal, a secret flag or any --set, only the required fields
|
|
403
|
+
// are asked for (and refused); an optional one is skipped.
|
|
404
|
+
const fields = asking || Object.keys(plain).length > 0
|
|
405
|
+
? plan.fields
|
|
406
|
+
: plan.fields.filter((field) => field.required);
|
|
407
|
+
if (interactive &&
|
|
408
|
+
!secretInputGiven(secret) &&
|
|
409
|
+
fields.some((field) => !(field.name in plain))) {
|
|
410
|
+
ctx.log(`${plan.product.name} for ${plan.scope === 'user' ? 'you, in' : 'team'} ${plan.team.slug}: enter what the app's form asks${plan.signIn === 'app' ? ' after the sign-in' : ''}. Secrets are hidden and never shown again.`);
|
|
411
|
+
}
|
|
412
|
+
const byName = new Map(plan.fields.map((field) => [field.name, field]));
|
|
413
|
+
const raw = fields.length === 0 && Object.keys(plain).length === 0
|
|
414
|
+
? {}
|
|
415
|
+
: await collectSecrets(ctx, {
|
|
416
|
+
fields: fields.map((field) => (Object.assign({ name: field.name, label: field.label, sensitive: field.sensitive, required: field.required }, (field.description ? { description: field.description } : {})))),
|
|
417
|
+
plain,
|
|
418
|
+
flags: secret,
|
|
419
|
+
io: teamsSecretIO(args),
|
|
420
|
+
interactive,
|
|
421
|
+
title: `${plan.product.name} for ${plan.team.slug}`,
|
|
422
|
+
refusal: `${plan.product.name} needs its form's fields (${fields
|
|
423
|
+
.filter((field) => field.required)
|
|
424
|
+
.map((field) => `${field.label} [${field.name}]`)
|
|
425
|
+
.join(', ')}), and there is no terminal to ask at.`,
|
|
426
|
+
again: again.replace(/^ekanos /, ''),
|
|
427
|
+
prompt: (spec) => {
|
|
428
|
+
var _a, _b;
|
|
429
|
+
const field = byName.get(spec.name);
|
|
430
|
+
if (field.description)
|
|
431
|
+
ctx.log(` ${field.description}`);
|
|
432
|
+
return (field.sensitive
|
|
433
|
+
? ((_a = args.askSecret) !== null && _a !== void 0 ? _a : promptHidden)
|
|
434
|
+
: ((_b = args.ask) !== null && _b !== void 0 ? _b : promptLine))(fieldPrompt(field));
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
const values = {};
|
|
438
|
+
for (const [name, text] of Object.entries(raw)) {
|
|
439
|
+
const field = byName.get(name);
|
|
440
|
+
if (!field)
|
|
441
|
+
continue;
|
|
442
|
+
const value = parseFieldValue(field, text);
|
|
443
|
+
if (value !== undefined)
|
|
444
|
+
values[name] = value;
|
|
445
|
+
}
|
|
446
|
+
const missing = plan.fields.filter((field) => field.required && values[field.name] === undefined);
|
|
447
|
+
if (missing.length > 0) {
|
|
448
|
+
throw validationError(`${missing.map((field) => `${field.label} (${field.name})`).join(', ')} ${missing.length === 1 ? 'is' : 'are'} needed.`, `Run "${again}" again.`);
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
fields: values,
|
|
452
|
+
set: Object.keys(plain).filter((name) => plain[name] !== ''),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
363
455
|
/** `--link-only`: the team's page and whether it is connected. */
|
|
364
456
|
async function printLink(ctx, client, host, target) {
|
|
365
457
|
var _a;
|