@fourier-labs/harbour 0.1.24 → 0.1.25
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { loadDatabaseGate } from "./database-gate.js";
|
|
4
|
-
import { CoverageLedger, createCoverageProxy, declaredCapabilities, missingFlowOperations, sourceTableVerbs } from "./flow-coverage.js";
|
|
4
|
+
import { CoverageLedger, capabilityCallSurface, createCoverageProxy, declaredCapabilities, flowDetail, missingFlowOperations, orphanedChecks, sourceTableVerbs } from "./flow-coverage.js";
|
|
5
5
|
import { CliError } from "./output.js";
|
|
6
6
|
import { DEPENDENT_READ_OPERATIONS, READ_OPERATIONS, kitPaths, readDeclaration, readKitLock, resourceNames, sourceDigest } from "./kit.js";
|
|
7
7
|
import { LOCAL, LocalRuntime, identityEnvironment, runCommand, runningOrigin } from "./local-runtime.js";
|
|
@@ -72,17 +72,25 @@ export async function runChecks(root, options) {
|
|
|
72
72
|
const appUrl = await proxy.listen();
|
|
73
73
|
try {
|
|
74
74
|
for (const name of journeys) {
|
|
75
|
+
// The journeys run one at a time, so naming the running check makes the
|
|
76
|
+
// gateway traffic that follows attributable to it — which is what lets
|
|
77
|
+
// an orphaned check be named as the file to delete.
|
|
78
|
+
ledger.nowRunning(name);
|
|
75
79
|
const result = await run("node", [join(kitPaths(root).checks, name)], { cwd: root, quiet: true, env: { HARBOUR_APP_URL: appUrl, HARBOUR_SDK_MODULE: join(root, "node_modules", "@harbour", "app-sdk", "dist", "index.js"), ...identityEnvironment(session) } });
|
|
80
|
+
ledger.nowRunning(undefined);
|
|
76
81
|
record(`journey:${name}`, result.code === 0 ? "pass" : "fail", result.code === 0 ? undefined : lastLines(result.stderr || result.stdout));
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
finally {
|
|
85
|
+
ledger.nowRunning(undefined);
|
|
80
86
|
await proxy.close();
|
|
81
87
|
}
|
|
88
|
+
// Both directions of the pipeline's gate: an operation or capability the
|
|
89
|
+
// app has with no check exercising it, and a check exercising a capability
|
|
90
|
+
// the app does not have.
|
|
82
91
|
const missing = missingFlowOperations(await sourceTableVerbs(root), await declaredCapabilities(root), ledger);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
: "every operation the app performs and every capability it declares was exercised by a retained check");
|
|
92
|
+
const orphaned = orphanedChecks(await capabilityCallSurface(root), ledger);
|
|
93
|
+
record("flow", missing.length || orphaned.length ? "fail" : "pass", flowDetail(missing, orphaned));
|
|
86
94
|
}
|
|
87
95
|
let integrations = "not tested";
|
|
88
96
|
if (options.governance && lock?.appId && !declaration.errors.length) {
|
|
@@ -25,16 +25,58 @@ export const COVERED_CAPABILITIES = ["data", "files", "actions", "telemetry", "r
|
|
|
25
25
|
const SDK_CAPABILITIES = ["data", "files", "actions", "realtime", "telemetry", "integrations"];
|
|
26
26
|
const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs"]);
|
|
27
27
|
const SKIPPED_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".harbour"]);
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* TypeScript call sites carry explicit type arguments between a method name and
|
|
30
|
+
* its call — `harbour.data.from<Note>("notes")`, `.select<Row>(` — and the
|
|
31
|
+
* starter's own list is one of them. Matching only the bare `.from(` made every
|
|
32
|
+
* typed call site invisible, so a freshly generated starter inventoried
|
|
33
|
+
* `notes: delete, insert, update` with no SELECT: the gate was reading the
|
|
34
|
+
* starter's own capabilities wrong. This is the pipeline's own
|
|
35
|
+
* `appSDKTypeArguments` (transformbuild/source_inspection.go), one nesting
|
|
36
|
+
* level of generics, so both scanners see the same call sites.
|
|
37
|
+
*/
|
|
38
|
+
const TYPE_ARGUMENTS = String.raw `(?:<[^<>()]*(?:<[^<>()]*>[^<>()]*)*>)?\s*`;
|
|
39
|
+
const TABLE_CHAIN_SOURCE = String.raw `\.from${TYPE_ARGUMENTS}\(\s*["'\`]([A-Za-z0-9_]+)["'\`]\s*\)`;
|
|
40
|
+
const TABLE_CHAIN = new RegExp(TABLE_CHAIN_SOURCE, "g");
|
|
41
|
+
/** The same chain, unanchored and non-global: where THIS chain's verbs stop. */
|
|
42
|
+
const NEXT_TABLE_CHAIN = new RegExp(TABLE_CHAIN_SOURCE);
|
|
43
|
+
const VERB_CALL = new RegExp(String.raw `\.(select|insert|update|delete|upsert)\s*${TYPE_ARGUMENTS}\(`, "g");
|
|
30
44
|
/** `createClient()` bindings name the identifier a capability call must be made on. */
|
|
31
45
|
const CLIENT_BINDING = /(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?::[^=]+)?=\s*(?:await\s+)?createClient\s*\(/g;
|
|
46
|
+
/** `<anything>.<namespace>.<method>(` — the receiver is deliberately not constrained; see capabilityCallSurface. */
|
|
47
|
+
const CAPABILITY_CALL = new RegExp(String.raw `\.\s*(${SDK_CAPABILITIES.join("|")})\s*\.\s*[A-Za-z_$][A-Za-z0-9_$]*\s*${TYPE_ARGUMENTS}\(`, "g");
|
|
32
48
|
/** Records the gateway operations observed while the retained checks ran. */
|
|
33
49
|
export class CoverageLedger {
|
|
34
50
|
seen = new Set();
|
|
51
|
+
/** capability -> the retained checks that asked the gateway for it. */
|
|
52
|
+
asked = new Map();
|
|
53
|
+
running;
|
|
35
54
|
add(key) { this.seen.add(key); }
|
|
36
55
|
has(key) { return this.seen.has(key); }
|
|
37
56
|
keys() { return [...this.seen].sort(); }
|
|
57
|
+
/**
|
|
58
|
+
* Names the retained check whose gateway traffic follows. The journeys run
|
|
59
|
+
* one at a time, so a request the proxy sees between this call and the next
|
|
60
|
+
* belongs to this check — which is how a refusal can name the file to delete.
|
|
61
|
+
*/
|
|
62
|
+
nowRunning(check) { this.running = check; }
|
|
63
|
+
/**
|
|
64
|
+
* One request the running check sent at a capability-gated path, recorded
|
|
65
|
+
* whatever the answer was. The deployed gateway resolves the capability
|
|
66
|
+
* BEFORE any handler runs and answers 404 when the app did not declare it
|
|
67
|
+
* (appgateway/resolver.go "capability is not declared"), so the request
|
|
68
|
+
* itself — not its local success — is what decides the check's fate there.
|
|
69
|
+
*/
|
|
70
|
+
askedFor(path) {
|
|
71
|
+
const capability = capabilityForRequest(path);
|
|
72
|
+
if (!capability || !this.running)
|
|
73
|
+
return;
|
|
74
|
+
const checks = this.asked.get(capability) ?? new Set();
|
|
75
|
+
checks.add(this.running);
|
|
76
|
+
this.asked.set(capability, checks);
|
|
77
|
+
}
|
|
78
|
+
/** capability -> the checks that asked for it, for the reverse coverage question. */
|
|
79
|
+
capabilitiesAsked() { return this.asked; }
|
|
38
80
|
/**
|
|
39
81
|
* The gateway's data boundary. A write that changed no row proves nothing, so
|
|
40
82
|
* only a select — or a mutation that returned rows — counts (observeData in
|
|
@@ -82,7 +124,7 @@ export async function sourceTableVerbs(root) {
|
|
|
82
124
|
const semicolon = tail.indexOf(";");
|
|
83
125
|
if (/[;\n]/.test(tail) && semicolon >= 0)
|
|
84
126
|
tail = tail.slice(0, semicolon);
|
|
85
|
-
const next = tail.
|
|
127
|
+
const next = tail.search(NEXT_TABLE_CHAIN);
|
|
86
128
|
if (next > 0)
|
|
87
129
|
tail = tail.slice(0, next);
|
|
88
130
|
tail = tail.slice(0, 200);
|
|
@@ -119,6 +161,101 @@ export async function declaredCapabilities(root) {
|
|
|
119
161
|
}
|
|
120
162
|
return used;
|
|
121
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Every capability namespace the app's own source calls a method on, whatever
|
|
166
|
+
* object the call is made on: `harbour.files.list(`, `client().files.list(`,
|
|
167
|
+
* `(harbour as {files: F}).files.list(`.
|
|
168
|
+
*
|
|
169
|
+
* This is deliberately looser than `declaredCapabilities`, and the two answer
|
|
170
|
+
* different questions. The forward gate asks "was this capability exercised?"
|
|
171
|
+
* and must be precise about what the app really uses. The reverse gate below
|
|
172
|
+
* asks "is this check exercising something the app no longer has?" — a
|
|
173
|
+
* question whose wrong answer refuses a good app — so it needs a lower bound on
|
|
174
|
+
* what is unused, never an upper bound on what is used.
|
|
175
|
+
*
|
|
176
|
+
* The pipeline's own evidence for a capability is `<client>.<namespace>.<method>(`
|
|
177
|
+
* with the client traced through the module graph (transformbuild
|
|
178
|
+
* analyzeAppSDKClientCalls / analyzeAppSDKNamespaceWrappers). Dropping the
|
|
179
|
+
* receiver makes this a superset of that set — every call site the pipeline can
|
|
180
|
+
* trace, plus ones it cannot — so a capability this cannot see is one the kit
|
|
181
|
+
* lane cannot declare either, and the reverse gate refuses only trees the
|
|
182
|
+
* pipeline already refuses. Where it errs it errs by staying quiet: a
|
|
183
|
+
* commented-out call still counts here, and the pipeline masks comments.
|
|
184
|
+
*/
|
|
185
|
+
export async function capabilityCallSurface(root) {
|
|
186
|
+
const surface = new Set();
|
|
187
|
+
for (const path of await sourceFiles(root)) {
|
|
188
|
+
const text = await readFile(path, "utf8").catch(() => "");
|
|
189
|
+
for (const match of text.matchAll(CAPABILITY_CALL))
|
|
190
|
+
surface.add(match[1]);
|
|
191
|
+
}
|
|
192
|
+
return surface;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The gateway's own path -> capability map (appgateway/gateway.go
|
|
196
|
+
* `capabilityForRequest`). Every one of these paths is resolved against the
|
|
197
|
+
* app's declared capability set before it is served.
|
|
198
|
+
*/
|
|
199
|
+
export function capabilityForRequest(path) {
|
|
200
|
+
if (path === "/_harbour/realtime")
|
|
201
|
+
return "realtime";
|
|
202
|
+
if (path.startsWith("/_harbour/files"))
|
|
203
|
+
return "files";
|
|
204
|
+
if (path.startsWith("/_harbour/actions/"))
|
|
205
|
+
return "actions";
|
|
206
|
+
if (path === "/_harbour/telemetry")
|
|
207
|
+
return "telemetry";
|
|
208
|
+
if (path.startsWith("/_harbour/integrations/"))
|
|
209
|
+
return "integrations";
|
|
210
|
+
if (path.startsWith("/_harbour/data/"))
|
|
211
|
+
return "data";
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The other direction, and the one that let a real deploy fail twice: a
|
|
216
|
+
* retained check exercising a capability the app does not have any more.
|
|
217
|
+
*
|
|
218
|
+
* A builder's agent replaced the starter's notes schema and deleted the
|
|
219
|
+
* "Private files" section but left `.harbour/checks/files-journey.mjs` behind.
|
|
220
|
+
* The kit lane derives `.harbour/app-capabilities.json` from the browser SDK
|
|
221
|
+
* surface, the cell's App Gateway serves only what that file declares, and the
|
|
222
|
+
* journey's first `harbour.files.*` call came back 404 — `flow.check-failed …
|
|
223
|
+
* files-journey.mjs: exit status 1`, twice, 2m23s and 2m12s. Locally the check
|
|
224
|
+
* was green: `harbour dev` hard-codes data/files/realtime/telemetry
|
|
225
|
+
* (local-runtime.ts `gatewayConfig`) and always provisions MinIO, so the
|
|
226
|
+
* orphaned journey passed on its own terms.
|
|
227
|
+
*
|
|
228
|
+
* This reproduces the pipeline's refusal rather than inventing a second
|
|
229
|
+
* opinion, at the pipeline's own granularity — the capability, not the table.
|
|
230
|
+
* The pipeline does have a table-level reverse check
|
|
231
|
+
* (`rejectServerTierOperationsDrivenFromTheBrowser`), but it is scoped to apps
|
|
232
|
+
* that declare a workload directory; a kit app has none, so a check that reads
|
|
233
|
+
* a table the browser no longer reads is served there and must be served here.
|
|
234
|
+
*/
|
|
235
|
+
export function orphanedChecks(surface, ledger) {
|
|
236
|
+
const orphaned = [];
|
|
237
|
+
for (const [capability, checks] of ledger.capabilitiesAsked()) {
|
|
238
|
+
if (surface.has(capability))
|
|
239
|
+
continue;
|
|
240
|
+
for (const check of checks) {
|
|
241
|
+
orphaned.push(`${check}: exercises the ${capability} capability, which no code in this app calls any more. ` +
|
|
242
|
+
`The deployed gateway serves only the capabilities the kit lane derives from the source, so it refuses this check as ` +
|
|
243
|
+
`flow.check-failed: ${check}: exit status 1. Delete .harbour/checks/${check}, or restore the harbour.${capability}.* feature it was written for.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return orphaned.sort();
|
|
247
|
+
}
|
|
248
|
+
/** The `flow` check's detail, in the pipeline's words, for both directions. */
|
|
249
|
+
export function flowDetail(missing, orphaned) {
|
|
250
|
+
const parts = [];
|
|
251
|
+
if (missing.length)
|
|
252
|
+
parts.push(`${missing.join("; ")}. Exercise the converted application operations with source-valid inputs and assertions; an unrelated passing check is insufficient.`);
|
|
253
|
+
if (orphaned.length)
|
|
254
|
+
parts.push(orphaned.join("; "));
|
|
255
|
+
if (!parts.length)
|
|
256
|
+
return "every operation the app performs and every capability it declares was exercised by a retained check, and no retained check exercises a capability the app no longer has";
|
|
257
|
+
return `${parts.join(" ")} The pipeline refuses this deployment as kit.check-failed: flow.check-failed.`;
|
|
258
|
+
}
|
|
122
259
|
/**
|
|
123
260
|
* What the pipeline would refuse, in its own words: an inventoried operation or
|
|
124
261
|
* a declared capability with no successful observation while the checks ran.
|
|
@@ -151,6 +288,10 @@ export function createCoverageProxy(origin, ledger) {
|
|
|
151
288
|
const upstreamOrigin = target.origin;
|
|
152
289
|
const server = createServer((incoming, response) => {
|
|
153
290
|
const path = (incoming.url ?? "/").split("?")[0];
|
|
291
|
+
// Before the answer: the deployed gateway refuses an undeclared capability
|
|
292
|
+
// at this path whatever the handler would have said, so what the check
|
|
293
|
+
// asked for is the evidence the reverse gate needs.
|
|
294
|
+
ledger.askedFor(path);
|
|
154
295
|
const chunks = [];
|
|
155
296
|
incoming.on("data", chunk => { if (chunks.length < 64)
|
|
156
297
|
chunks.push(chunk); });
|
|
@@ -177,6 +318,7 @@ export function createCoverageProxy(origin, ledger) {
|
|
|
177
318
|
// receives nothing does not clear the capability here either.
|
|
178
319
|
server.on("upgrade", (incoming, socket, head) => {
|
|
179
320
|
const path = (incoming.url ?? "/").split("?")[0];
|
|
321
|
+
ledger.askedFor(path);
|
|
180
322
|
const upstream = connect(port, host, () => {
|
|
181
323
|
const headers = Object.entries({ ...forwardable(incoming.headers, upstreamOrigin), host: `${host}:${port}`, connection: "Upgrade", upgrade: "websocket" })
|
|
182
324
|
.map(([key, value]) => `${key}: ${value}`);
|
|
@@ -105,43 +105,110 @@ export function renderFailure(envelope) {
|
|
|
105
105
|
const error = envelope.error;
|
|
106
106
|
return `${error?.message ?? NO_DETAIL}${error?.remediationHint ? ` ${error.remediationHint}` : ""}\n`;
|
|
107
107
|
}
|
|
108
|
-
/**
|
|
109
|
-
*
|
|
108
|
+
/**
|
|
109
|
+
* Server-supplied text on its way to a terminal, given exactly the treatment
|
|
110
|
+
* `safeError` gives the failure path: secrets stripped, whitespace collapsed,
|
|
111
|
+
* bounded at `DETAIL_BOUND` keeping both ends.
|
|
112
|
+
*
|
|
113
|
+
* `safeError` was `redact`’s only caller. `renderSummary` printed the server’s
|
|
114
|
+
* words straight from the payload, so on the success-shaped path — a deployment
|
|
115
|
+
* that reports a failure inside an otherwise-successful envelope, which is what
|
|
116
|
+
* `status` and `productionise` emit after a failed build — that text reached the
|
|
117
|
+
* terminal both unredacted and unbounded. A presigned URL, a bearer token or a
|
|
118
|
+
* forwarded header dump printed verbatim into whatever captured stdout, and a
|
|
119
|
+
* pathological message printed whole.
|
|
120
|
+
*
|
|
121
|
+
* That is the opposite defect to the one the tail-keeping change fixed: that
|
|
122
|
+
* one lost information, this one leaks and floods. One function and not a
|
|
123
|
+
* second implementation, because a second implementation is how the two paths
|
|
124
|
+
* came to differ at all.
|
|
125
|
+
*/
|
|
126
|
+
function said(value) {
|
|
127
|
+
return value ? redact(value) : "";
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Human-readable stdout for runs without `--json`: the operation reference and
|
|
131
|
+
* whatever link or next step Harbour reported, nothing internal.
|
|
132
|
+
*
|
|
133
|
+
* Every server-supplied string printed here goes through `said`. What does not
|
|
134
|
+
* is listed below, because each is a value whose whole job is to be reproduced
|
|
135
|
+
* verbatim, and each would be destroyed by the very rules that make redaction
|
|
136
|
+
* worth doing:
|
|
137
|
+
*
|
|
138
|
+
* - `protectedUrl`, `productionUrl` — `SECRETS` strips URLs, and these two are
|
|
139
|
+
* the app’s address: the thing the builder ran the command to get.
|
|
140
|
+
* - `operationRef` — the handle every later `--operation` needs, and already
|
|
141
|
+
* exempt on the failure path: `safeError` redacts the message and the hint,
|
|
142
|
+
* never the reference they travel with. The same policy on both paths.
|
|
143
|
+
* - `secrets.asks[].name` and `.prefilledFromPath` — identifiers, not prose. A
|
|
144
|
+
* secret named `AUTHORIZATION_TOKEN` matches the header pattern and would be
|
|
145
|
+
* erased whole, printing `Secret : needs a value`; the path is read off the
|
|
146
|
+
* local checkout rather than sent by the server.
|
|
147
|
+
* - `audience.emails` — the builder’s own confirmed list, echoed back. A
|
|
148
|
+
* 240-character bound over a joined list silently drops members, which is
|
|
149
|
+
* the information loss the tail-keeping change was about.
|
|
150
|
+
* - `ask.status`, `ask.scope`, `secrets.available`, `audience.confirmed` and
|
|
151
|
+
* the three mapped `pending` steps — compared against fixed literals, so
|
|
152
|
+
* only Harbour’s own words ever print. An unmapped `pending` step is the
|
|
153
|
+
* server’s own id, so that one does go through `said`.
|
|
154
|
+
*
|
|
155
|
+
* `deployment.message` and `production.message` are declared on the payload and
|
|
156
|
+
* never printed here; nothing routes them, and a line that printed one would
|
|
157
|
+
* need `said` like the rest. The `--json` envelope is unchanged: it carries the
|
|
158
|
+
* raw structured result by design and this is about what is printed. Whether
|
|
159
|
+
* the envelope should be redacted too is a decision about that contract, not
|
|
160
|
+
* one to take in the renderer.
|
|
161
|
+
*/
|
|
110
162
|
export function renderSummary(envelope) {
|
|
111
163
|
const lines = [];
|
|
112
164
|
const result = (envelope.result ?? {});
|
|
113
165
|
if (envelope.operationRef)
|
|
114
166
|
lines.push(`Operation reference: ${envelope.operationRef}`);
|
|
115
|
-
|
|
116
|
-
|
|
167
|
+
const verification = said(result.verification);
|
|
168
|
+
const assurance = said(result.assuranceLevel);
|
|
169
|
+
if (verification)
|
|
170
|
+
lines.push(`Saved app: ${verification}${assurance ? ` (${assurance})` : ""}`);
|
|
117
171
|
if (result.production) {
|
|
118
|
-
lines.push(`Production: ${result.production.state}${result.production.productionUrl ? ` — ${result.production.productionUrl}` : ""}`);
|
|
172
|
+
lines.push(`Production: ${said(result.production.state)}${result.production.productionUrl ? ` — ${result.production.productionUrl}` : ""}`);
|
|
173
|
+
// `|| NO_DETAIL` for the reason `safeError` has it: redaction can empty a
|
|
174
|
+
// string, and a failure reported as two spaces is worse than one that says
|
|
175
|
+
// plainly this CLI has nothing to show.
|
|
119
176
|
if (result.production.failure?.message)
|
|
120
|
-
lines.push(` ${result.production.failure.message}`);
|
|
177
|
+
lines.push(` ${said(result.production.failure.message) || NO_DETAIL}`);
|
|
121
178
|
}
|
|
122
179
|
if (result.deployment) {
|
|
123
|
-
lines.push(`Preview deployment: ${result.deployment.status ?? result.deployment.state}${result.deployment.protectedUrl ? ` — ${result.deployment.protectedUrl}` : ""}`);
|
|
124
|
-
if (result.deployment.failure?.message)
|
|
125
|
-
|
|
180
|
+
lines.push(`Preview deployment: ${said(result.deployment.status ?? result.deployment.state)}${result.deployment.protectedUrl ? ` — ${result.deployment.protectedUrl}` : ""}`);
|
|
181
|
+
if (result.deployment.failure?.message) {
|
|
182
|
+
// The hint is redacted and bounded on its own, exactly as `safeError`
|
|
183
|
+
// bounds it separately from the message: appending it first would spend
|
|
184
|
+
// the message’s own budget on it.
|
|
185
|
+
const hint = said(result.deployment.failure.remediationHint);
|
|
186
|
+
lines.push(` ${said(result.deployment.failure.message) || NO_DETAIL}${hint ? ` ${hint}` : ""}`);
|
|
187
|
+
}
|
|
126
188
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
189
|
+
// Gated on the redacted value rather than the raw one, so guidance that is
|
|
190
|
+
// nothing but a link falls through to the next step instead of printing a
|
|
191
|
+
// label with nothing after it.
|
|
192
|
+
const waiting = said(result.waiting?.plainEnglish);
|
|
193
|
+
const nextStep = said(result.nextStep);
|
|
194
|
+
if (waiting)
|
|
195
|
+
lines.push(`Action needed: ${waiting}`);
|
|
196
|
+
else if (nextStep)
|
|
197
|
+
lines.push(`Next: ${nextStep}`);
|
|
131
198
|
const setup = envelope.result?.setup;
|
|
132
199
|
const secrets = setup?.secrets ?? envelope.result?.secrets;
|
|
133
200
|
if (setup?.profile) {
|
|
134
|
-
lines.push(`App name: ${setup.profile.displayName
|
|
201
|
+
lines.push(`App name: ${said(setup.profile.displayName) || "(none)"}${setup.profile?.confirmed ? " (confirmed)" : setup.profile?.suggested?.displayName ? ` — suggested: ${said(setup.profile.suggested.displayName)}` : " (not confirmed)"}`);
|
|
135
202
|
if (setup.profile?.description || setup.profile?.suggested?.description)
|
|
136
|
-
lines.push(`Description: ${setup.profile?.confirmed ? setup.profile.description : setup.profile?.suggested?.description ?? setup.profile?.description}`);
|
|
203
|
+
lines.push(`Description: ${said(setup.profile?.confirmed ? setup.profile.description : setup.profile?.suggested?.description ?? setup.profile?.description)}`);
|
|
137
204
|
if (setup.audience?.available === false)
|
|
138
|
-
lines.push(`Audience: ${setup.audience.plainEnglish
|
|
205
|
+
lines.push(`Audience: ${said(setup.audience.plainEnglish) || "console only"}`);
|
|
139
206
|
else
|
|
140
207
|
lines.push(`Audience: ${setup.audience?.confirmed ? (setup.audience.emails?.length ? setup.audience.emails.join(", ") : "only you") + " (confirmed)" : "not confirmed"}`);
|
|
141
208
|
}
|
|
142
209
|
if (secrets) {
|
|
143
210
|
if (secrets.available === false)
|
|
144
|
-
lines.push(`Secrets: ${secrets.plainEnglish
|
|
211
|
+
lines.push(`Secrets: ${said(secrets.plainEnglish) || "console only"}`);
|
|
145
212
|
else if (!secrets.asks?.length)
|
|
146
213
|
lines.push("Secrets: none asked for");
|
|
147
214
|
else
|
|
@@ -149,7 +216,7 @@ export function renderSummary(envelope) {
|
|
|
149
216
|
lines.push(`Secret ${ask.name}: ${ask.status === "UNSET" ? "needs a value" : ask.status === "SET" ? "set" : "not needed"}${ask.scope === "personal" ? " (personal)" : ""}${ask.prefilledFromPath ? ` (from ${ask.prefilledFromPath})` : ""}`);
|
|
150
217
|
}
|
|
151
218
|
// productionise carries only the open steps; the setup commands carry the full view.
|
|
152
|
-
const openSteps = setup?.pending?.length ? setup.pending.map(step => step === "confirm_app_profile" ? "confirm the app's name and description" : step === "confirm_app_audience" ? "confirm who may open it" : step === "provide_secrets" ? "provide the secrets it asked for" : step) : [];
|
|
219
|
+
const openSteps = setup?.pending?.length ? setup.pending.map(step => step === "confirm_app_profile" ? "confirm the app's name and description" : step === "confirm_app_audience" ? "confirm who may open it" : step === "provide_secrets" ? "provide the secrets it asked for" : said(step)) : [];
|
|
153
220
|
if (openSteps.length)
|
|
154
221
|
lines.push(`Still needed: ${openSteps.join("; ")}${envelope.result.setup?.plainEnglish && !setup?.profile ? ` — run \`harbour setup --operation ${envelope.operationRef ?? "<reference>"}\`` : ""}`);
|
|
155
222
|
else if (setup?.profile)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.1.
|
|
1
|
+
export const CLI_VERSION = "0.1.25";
|