@chatlychat/cli 0.1.3 → 0.1.5
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/ai.d.ts +4 -0
- package/dist/commands/ai.d.ts.map +1 -0
- package/dist/commands/ai.js +149 -0
- package/dist/commands/ai.js.map +1 -0
- package/dist/commands/common.d.ts +79 -0
- package/dist/commands/common.d.ts.map +1 -0
- package/dist/commands/common.js +194 -0
- package/dist/commands/common.js.map +1 -0
- package/dist/commands/config.d.ts +40 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +336 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/insights.d.ts +5 -0
- package/dist/commands/insights.d.ts.map +1 -0
- package/dist/commands/insights.js +277 -0
- package/dist/commands/insights.js.map +1 -0
- package/dist/commands/integrations.d.ts +15 -0
- package/dist/commands/integrations.d.ts.map +1 -0
- package/dist/commands/integrations.js +256 -0
- package/dist/commands/integrations.js.map +1 -0
- package/dist/commands/resources.d.ts +19 -7
- package/dist/commands/resources.d.ts.map +1 -1
- package/dist/commands/resources.js +100 -191
- package/dist/commands/resources.js.map +1 -1
- package/dist/commands/workspace-as-code.d.ts +27 -0
- package/dist/commands/workspace-as-code.d.ts.map +1 -0
- package/dist/commands/workspace-as-code.js +377 -0
- package/dist/commands/workspace-as-code.js.map +1 -0
- package/dist/help.d.ts +25 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +104 -8
- package/dist/help.js.map +1 -1
- package/dist/run.d.ts +17 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +88 -41
- package/dist/run.js.map +1 -1
- package/dist/wac/__tests__/wac-fixtures.d.ts +20 -0
- package/dist/wac/__tests__/wac-fixtures.d.ts.map +1 -0
- package/dist/wac/__tests__/wac-fixtures.js +38 -0
- package/dist/wac/__tests__/wac-fixtures.js.map +1 -0
- package/dist/wac/apply.d.ts +113 -0
- package/dist/wac/apply.d.ts.map +1 -0
- package/dist/wac/apply.js +265 -0
- package/dist/wac/apply.js.map +1 -0
- package/dist/wac/diff.d.ts +137 -0
- package/dist/wac/diff.d.ts.map +1 -0
- package/dist/wac/diff.js +413 -0
- package/dist/wac/diff.js.map +1 -0
- package/dist/wac/document.d.ts +110 -0
- package/dist/wac/document.d.ts.map +1 -0
- package/dist/wac/document.js +429 -0
- package/dist/wac/document.js.map +1 -0
- package/dist/wac/render.d.ts +52 -0
- package/dist/wac/render.d.ts.map +1 -0
- package/dist/wac/render.js +156 -0
- package/dist/wac/render.js.map +1 -0
- package/dist/wac/resource-def.d.ts +116 -0
- package/dist/wac/resource-def.d.ts.map +1 -0
- package/dist/wac/resource-def.js +44 -0
- package/dist/wac/resource-def.js.map +1 -0
- package/dist/wac/resources.d.ts +104 -0
- package/dist/wac/resources.d.ts.map +1 -0
- package/dist/wac/resources.js +541 -0
- package/dist/wac/resources.js.map +1 -0
- package/dist/wac/values.d.ts +16 -0
- package/dist/wac/values.d.ts.map +1 -0
- package/dist/wac/values.js +53 -0
- package/dist/wac/values.js.map +1 -0
- package/package.json +3 -2
- package/dist/cli.spec.d.ts +0 -2
- package/dist/cli.spec.d.ts.map +0 -1
- package/dist/cli.spec.js +0 -226
- package/dist/cli.spec.js.map +0 -1
package/dist/run.js
CHANGED
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { ChatlyApiError, ChatlyClient, ChatlyTransportError } from '@chatlychat/sdk-node';
|
|
10
10
|
import { bool, parseArgs, str } from './args.js';
|
|
11
|
+
import * as ai from './commands/ai.js';
|
|
11
12
|
import * as auth from './commands/auth.js';
|
|
13
|
+
import * as config from './commands/config.js';
|
|
14
|
+
import * as insights from './commands/insights.js';
|
|
15
|
+
import * as integrations from './commands/integrations.js';
|
|
12
16
|
import * as cmd from './commands/resources.js';
|
|
17
|
+
import * as wac from './commands/workspace-as-code.js';
|
|
13
18
|
import { resolveCredentials } from './config.js';
|
|
14
19
|
import { HELP } from './help.js';
|
|
15
20
|
/**
|
|
@@ -21,9 +26,82 @@ import { HELP } from './help.js';
|
|
|
21
26
|
* drifts — it said `0.1.0` while the published package was `0.1.2` — so
|
|
22
27
|
* `cli.spec.ts` asserts the two agree and fails if they do not.
|
|
23
28
|
*/
|
|
24
|
-
const VERSION = '0.1.
|
|
29
|
+
const VERSION = '0.1.5';
|
|
25
30
|
/** Commands that need no credential. Everything else resolves a key first. */
|
|
26
31
|
const UNAUTHENTICATED = new Set(['login', 'logout', 'help', 'version']);
|
|
32
|
+
/**
|
|
33
|
+
* Every authenticated command, by the word a person types.
|
|
34
|
+
*
|
|
35
|
+
* A table rather than a `switch`, for one reason that is worth the churn:
|
|
36
|
+
* `cli.spec.ts` can now assert that every key here appears in `--help` and
|
|
37
|
+
* that every command documented in `--help` is reachable. A command with no
|
|
38
|
+
* help entry is a command nobody finds; a help entry with no command is a
|
|
39
|
+
* promise the CLI breaks. The `switch` this replaced could be checked for
|
|
40
|
+
* neither.
|
|
41
|
+
*
|
|
42
|
+
* Singular aliases are listed explicitly rather than derived by stripping an
|
|
43
|
+
* `s`, so `kb`, `search`, `whoami` and `ai` cannot acquire a nonsense twin.
|
|
44
|
+
*/
|
|
45
|
+
export const COMMANDS = {
|
|
46
|
+
// ── Core ──────────────────────────────────────────────────────────────
|
|
47
|
+
whoami: async (c, a) => auth.whoami(c, bool(a.flags, 'json')),
|
|
48
|
+
conversations: cmd.conversations,
|
|
49
|
+
contacts: cmd.contacts,
|
|
50
|
+
tickets: cmd.tickets,
|
|
51
|
+
search: cmd.search,
|
|
52
|
+
kb: cmd.kb,
|
|
53
|
+
events: cmd.events,
|
|
54
|
+
workspace: cmd.workspace,
|
|
55
|
+
// ── Config as code ────────────────────────────────────────────────────
|
|
56
|
+
bots: config.bots,
|
|
57
|
+
'bot-routes': config.botRoutes,
|
|
58
|
+
triggers: config.triggers,
|
|
59
|
+
brands: config.brands,
|
|
60
|
+
'email-templates': config.emailTemplates,
|
|
61
|
+
macros: config.macros,
|
|
62
|
+
'knowledge-gaps': config.knowledgeGaps,
|
|
63
|
+
// ── Integrations ──────────────────────────────────────────────────────
|
|
64
|
+
webhooks: integrations.webhooks,
|
|
65
|
+
'customer-cards': integrations.customerCards,
|
|
66
|
+
'docs-sources': integrations.docsSources,
|
|
67
|
+
channels: integrations.channels,
|
|
68
|
+
// ── Workspace as code ─────────────────────────────────────────────────
|
|
69
|
+
/*
|
|
70
|
+
* `plan` and `apply` are TWO entries rather than one `config` group with
|
|
71
|
+
* two subcommands, because `chatly plan` / `chatly apply` is what the
|
|
72
|
+
* integrator asked for by name and what anybody who has used terraform
|
|
73
|
+
* will type. Both read one JSON document and cover nine resource types; a
|
|
74
|
+
* group word in front would only make the muscle memory wrong.
|
|
75
|
+
*/
|
|
76
|
+
plan: wac.plan,
|
|
77
|
+
apply: wac.apply,
|
|
78
|
+
// ── AI ────────────────────────────────────────────────────────────────
|
|
79
|
+
ai: ai.ai,
|
|
80
|
+
// ── Numbers ───────────────────────────────────────────────────────────
|
|
81
|
+
reports: insights.reports,
|
|
82
|
+
billing: insights.billing,
|
|
83
|
+
// ── Escape hatch ──────────────────────────────────────────────────────
|
|
84
|
+
api: cmd.api,
|
|
85
|
+
};
|
|
86
|
+
/** Singular spellings, for the groups where typing one reads naturally. */
|
|
87
|
+
export const ALIASES = {
|
|
88
|
+
conversation: 'conversations',
|
|
89
|
+
contact: 'contacts',
|
|
90
|
+
ticket: 'tickets',
|
|
91
|
+
event: 'events',
|
|
92
|
+
webhook: 'webhooks',
|
|
93
|
+
report: 'reports',
|
|
94
|
+
channel: 'channels',
|
|
95
|
+
bot: 'bots',
|
|
96
|
+
'bot-route': 'bot-routes',
|
|
97
|
+
trigger: 'triggers',
|
|
98
|
+
brand: 'brands',
|
|
99
|
+
macro: 'macros',
|
|
100
|
+
'email-template': 'email-templates',
|
|
101
|
+
'customer-card': 'customer-cards',
|
|
102
|
+
'docs-source': 'docs-sources',
|
|
103
|
+
'knowledge-gap': 'knowledge-gaps',
|
|
104
|
+
};
|
|
27
105
|
const defaultFactory = (opts) => new ChatlyClient(opts);
|
|
28
106
|
export async function run(argv, makeClient = defaultFactory) {
|
|
29
107
|
const args = parseArgs(argv);
|
|
@@ -53,51 +131,19 @@ export async function run(argv, makeClient = defaultFactory) {
|
|
|
53
131
|
return auth.logout();
|
|
54
132
|
}
|
|
55
133
|
if (!UNAUTHENTICATED.has(command)) {
|
|
134
|
+
const name = ALIASES[command] ?? command;
|
|
135
|
+
const handler = COMMANDS[name];
|
|
136
|
+
if (handler === undefined) {
|
|
137
|
+
process.stderr.write(`Unknown command "${command}". Run \`chatly --help\`.\n`);
|
|
138
|
+
return 2;
|
|
139
|
+
}
|
|
56
140
|
// Resolution order and the reason for it are in config.ts.
|
|
57
141
|
const creds = resolveCredentials(str(args.flags, 'key'), str(args.flags, 'api-url'));
|
|
58
142
|
const chatly = makeClient({
|
|
59
143
|
apiKey: creds.apiKey,
|
|
60
144
|
...(creds.apiUrl === undefined ? {} : { apiUrl: creds.apiUrl }),
|
|
61
145
|
});
|
|
62
|
-
|
|
63
|
-
switch (command) {
|
|
64
|
-
case 'whoami':
|
|
65
|
-
return auth.whoami(chatly, json);
|
|
66
|
-
case 'conversations':
|
|
67
|
-
case 'conversation':
|
|
68
|
-
return cmd.conversations(chatly, args);
|
|
69
|
-
case 'contacts':
|
|
70
|
-
case 'contact':
|
|
71
|
-
return cmd.contacts(chatly, args);
|
|
72
|
-
case 'tickets':
|
|
73
|
-
case 'ticket':
|
|
74
|
-
return cmd.tickets(chatly, args);
|
|
75
|
-
case 'search':
|
|
76
|
-
return cmd.search(chatly, args);
|
|
77
|
-
case 'kb':
|
|
78
|
-
return cmd.kb(chatly, args);
|
|
79
|
-
case 'events':
|
|
80
|
-
case 'event':
|
|
81
|
-
return cmd.events(chatly, args);
|
|
82
|
-
case 'webhooks':
|
|
83
|
-
case 'webhook':
|
|
84
|
-
return cmd.webhooks(chatly, args);
|
|
85
|
-
case 'reports':
|
|
86
|
-
case 'report':
|
|
87
|
-
return cmd.reports(chatly, args);
|
|
88
|
-
case 'workspace':
|
|
89
|
-
return cmd.workspace(chatly, args);
|
|
90
|
-
case 'channels':
|
|
91
|
-
case 'channel':
|
|
92
|
-
return cmd.channels(chatly, args);
|
|
93
|
-
case 'billing':
|
|
94
|
-
return cmd.billing(chatly, args);
|
|
95
|
-
case 'api':
|
|
96
|
-
return cmd.api(chatly, args);
|
|
97
|
-
default:
|
|
98
|
-
process.stderr.write(`Unknown command "${command}". Run \`chatly --help\`.\n`);
|
|
99
|
-
return 2;
|
|
100
|
-
}
|
|
146
|
+
return handler(chatly, args);
|
|
101
147
|
}
|
|
102
148
|
process.stdout.write(HELP);
|
|
103
149
|
return 0;
|
|
@@ -116,7 +162,8 @@ export function explain(err) {
|
|
|
116
162
|
const hint = err.status === 401
|
|
117
163
|
? '\nThe key was rejected. Check it with `chatly whoami`, or run `chatly login` again.'
|
|
118
164
|
: err.status === 403
|
|
119
|
-
? '\nThe key is valid but lacks a scope for this route. `chatly whoami` lists what it
|
|
165
|
+
? '\nThe key is valid but lacks a scope for this route. `chatly whoami` lists what it ' +
|
|
166
|
+
'has, and `chatly workspace key-scopes` lists every scope there is.'
|
|
120
167
|
: err.status === 429
|
|
121
168
|
? `\nRate limited.${err.retryAfter === null ? '' : ` Retry after ${String(err.retryAfter)}s.`}`
|
|
122
169
|
: '';
|
package/dist/run.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE1F,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared fixtures for the workspace-as-code suites.
|
|
3
|
+
*
|
|
4
|
+
* Extracted when `wac.spec.ts` reached 717 of the 650 lines
|
|
5
|
+
* `check-file-size` allows. Split at the FIXTURE boundary rather than by
|
|
6
|
+
* trimming assertions: there is not one assertion in this file, so a reader
|
|
7
|
+
* looking for what is pinned still finds all of it in a `*.spec.ts`.
|
|
8
|
+
*/
|
|
9
|
+
import { type ApplyOptions } from '../apply.js';
|
|
10
|
+
import { type LiveSection } from '../diff.js';
|
|
11
|
+
import { type ResourceDef } from '../resources.js';
|
|
12
|
+
export declare const APPLY: ApplyOptions;
|
|
13
|
+
export declare function doc(resources: Record<string, unknown>, extra?: Record<string, unknown>): {
|
|
14
|
+
version: number;
|
|
15
|
+
resources: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
export declare function issues(input: unknown, env?: Record<string, string>): string[];
|
|
18
|
+
export declare function live(rows: Record<string, ReadonlyArray<Record<string, unknown>>>): Map<string, LiveSection>;
|
|
19
|
+
export declare function def(type: string): ResourceDef;
|
|
20
|
+
//# sourceMappingURL=wac-fixtures.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wac-fixtures.d.ts","sourceRoot":"","sources":["../../../src/wac/__tests__/wac-fixtures.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEhE,eAAO,MAAM,KAAK,EAAE,YAAkE,CAAC;AAEvF,wBAAgB,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;;;EAE1F;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,MAAM,EAAE,CAUjF;AAED,wBAAgB,IAAI,CAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,GAC3D,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAE1B;AAED,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAM7C"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared fixtures for the workspace-as-code suites.
|
|
3
|
+
*
|
|
4
|
+
* Extracted when `wac.spec.ts` reached 717 of the 650 lines
|
|
5
|
+
* `check-file-size` allows. Split at the FIXTURE boundary rather than by
|
|
6
|
+
* trimming assertions: there is not one assertion in this file, so a reader
|
|
7
|
+
* looking for what is pinned still finds all of it in a `*.spec.ts`.
|
|
8
|
+
*/
|
|
9
|
+
import { DocumentError, parseDocument } from '../document.js';
|
|
10
|
+
import { resourceDef } from '../resources.js';
|
|
11
|
+
export const APPLY = { prune: false, pruneUnsafe: [], writeOnly: false };
|
|
12
|
+
export function doc(resources, extra = {}) {
|
|
13
|
+
return { version: 1, resources, ...extra };
|
|
14
|
+
}
|
|
15
|
+
export function issues(input, env = {}) {
|
|
16
|
+
try {
|
|
17
|
+
parseDocument(input, env);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
if (err instanceof DocumentError) {
|
|
21
|
+
return err.issues.map((i) => `${i.path}: ${i.message}`);
|
|
22
|
+
}
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
export function live(rows) {
|
|
28
|
+
return new Map(Object.entries(rows).map(([k, v]) => [k, { rows: v }]));
|
|
29
|
+
}
|
|
30
|
+
export function def(type) {
|
|
31
|
+
const found = resourceDef(type);
|
|
32
|
+
if (found === undefined) {
|
|
33
|
+
throw new Error(`no such resource type: ${type}`);
|
|
34
|
+
}
|
|
35
|
+
return found;
|
|
36
|
+
}
|
|
37
|
+
/* ══ The schema ═══════════════════════════════════════════════════════════ */
|
|
38
|
+
//# sourceMappingURL=wac-fixtures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wac-fixtures.js","sourceRoot":"","sources":["../../../src/wac/__tests__/wac-fixtures.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAoB,MAAM,iBAAiB,CAAC;AAEhE,MAAM,CAAC,MAAM,KAAK,GAAiB,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;AAEvF,MAAM,UAAU,GAAG,CAAC,SAAkC,EAAE,QAAiC,EAAE;IACzF,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,KAAc,EAAE,MAA8B,EAAE;IACrE,IAAI,CAAC;QACH,aAAa,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAC5B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,aAAa,EAAE,CAAC;YACjC,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QAC1D,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,IAAI,CAClB,IAA4D;IAE5D,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,IAAY;IAC9B,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,+EAA+E"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `apply`: turning a plan into writes, and the three decisions that shape it.
|
|
3
|
+
*
|
|
4
|
+
* ═══ 1. PARTIAL FAILURE: CONTINUE, SKIP DEPENDENTS, EXIT NON-ZERO ════════
|
|
5
|
+
*
|
|
6
|
+
* Twelve resources, and the seventh answers 400. Stopping there leaves six
|
|
7
|
+
* applied and five untried; continuing leaves eleven applied and one failed.
|
|
8
|
+
* Both are half-applied — there is no transaction across nine REST
|
|
9
|
+
* collections and pretending otherwise would be a lie — so the choice is
|
|
10
|
+
* about which half-applied state is more useful, and continuing wins for
|
|
11
|
+
* three reasons:
|
|
12
|
+
*
|
|
13
|
+
* - It reports every problem in one run. A document with four bad rows
|
|
14
|
+
* takes one `apply` to find out, not four.
|
|
15
|
+
* - The ones that succeeded are IDEMPOTENT to re-run. Every write carries
|
|
16
|
+
* an `Idempotency-Key` (CONTRACT.md) and, more to the point, the next
|
|
17
|
+
* `plan` recomputes from live state, so a succeeded resource is simply a
|
|
18
|
+
* `noop` next time. Stopping early buys nothing that re-running does not.
|
|
19
|
+
* - Stopping at the seventh tells you nothing about the eighth, and the
|
|
20
|
+
* eighth is frequently the same mistake.
|
|
21
|
+
*
|
|
22
|
+
* The one thing continuing must NOT do is cascade. A routing rule that names
|
|
23
|
+
* a bot whose create just failed is SKIPPED, not attempted: attempting it
|
|
24
|
+
* produces a second, confusing error about a null `botId` that sends the
|
|
25
|
+
* reader to the wrong place. `dependsOn` on each plan item is what makes that
|
|
26
|
+
* possible, and the skip says which failure caused it.
|
|
27
|
+
*
|
|
28
|
+
* Whatever happened, the final output is three explicit buckets — applied,
|
|
29
|
+
* failed, skipped — and the exit code is 1 if either of the last two is
|
|
30
|
+
* non-empty. "Which resources changed" is never left to inference.
|
|
31
|
+
*
|
|
32
|
+
* ═══ 2. SECRETS: PRINTED ONCE, STORED NOWHERE ════════════════════════════
|
|
33
|
+
*
|
|
34
|
+
* Four creates on this surface answer with a credential that no route will
|
|
35
|
+
* ever read back: a bot's `signingSecret` and its `cb_` token, a customer
|
|
36
|
+
* card's `cc_` secret (returned BESIDE the card, not inside it), and a
|
|
37
|
+
* webhook endpoint's `whsec_`. Losing one means delete and recreate.
|
|
38
|
+
*
|
|
39
|
+
* So `apply` prints them, in a block of their own, at the end, where they
|
|
40
|
+
* cannot scroll past unnoticed — and writes them nowhere. Not into the
|
|
41
|
+
* document, not into a state file, not into `~/.config/chatly`. Writing a
|
|
42
|
+
* credential into a file that lives in git is the failure this feature would
|
|
43
|
+
* otherwise industrialise, and the document is the one file we KNOW is in
|
|
44
|
+
* git. There is deliberately no `--save-secrets`.
|
|
45
|
+
*
|
|
46
|
+
* The sweep is belt and braces: the declared `secrets` list per resource,
|
|
47
|
+
* PLUS anything whose field name reads like a credential. A hand-written list
|
|
48
|
+
* goes stale, and CLAUDE.md records twice over what that looks like — a
|
|
49
|
+
* gate's green line counting what it managed to parse rather than what
|
|
50
|
+
* exists. A field called `webhookToken` that nobody told `resources.ts` about
|
|
51
|
+
* is still surfaced.
|
|
52
|
+
*
|
|
53
|
+
* ═══ 3. THE BODY IS BUILT FROM THE DIFF, NOT FROM THE DOCUMENT ═══════════
|
|
54
|
+
*
|
|
55
|
+
* An update sends only the fields that changed, to the route that owns them —
|
|
56
|
+
* which is how a bot's `webhookUrl` reaches `/v1/bots/{id}/external` while
|
|
57
|
+
* its `model` reaches `/v1/bots/{id}`. The exception is a `fullReplace`
|
|
58
|
+
* resource, where a partial body would RESET everything it omits: `triggers`
|
|
59
|
+
* writes every settable field unconditionally, and an email template's PUT
|
|
60
|
+
* takes `{ subject, body }` and nothing else. For those the whole declared
|
|
61
|
+
* row goes over, and `plan` says so on the line so nobody finds out from the
|
|
62
|
+
* result.
|
|
63
|
+
*/
|
|
64
|
+
import { type ResourceDef } from './resources.js';
|
|
65
|
+
import type { PlanItem } from './diff.js';
|
|
66
|
+
export interface WriteOp {
|
|
67
|
+
readonly method: 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
|
68
|
+
readonly path: string;
|
|
69
|
+
readonly body?: Record<string, unknown>;
|
|
70
|
+
}
|
|
71
|
+
export interface FoundSecret {
|
|
72
|
+
readonly type: string;
|
|
73
|
+
readonly key: string;
|
|
74
|
+
readonly field: string;
|
|
75
|
+
readonly value: string;
|
|
76
|
+
}
|
|
77
|
+
export interface ApplyOutcome {
|
|
78
|
+
readonly type: string;
|
|
79
|
+
readonly key: string;
|
|
80
|
+
readonly action: PlanItem['action'];
|
|
81
|
+
readonly state: 'applied' | 'failed' | 'skipped' | 'kept';
|
|
82
|
+
/** The failure, or the reason for a skip. */
|
|
83
|
+
readonly detail: string | undefined;
|
|
84
|
+
readonly secrets: ReadonlyArray<FoundSecret>;
|
|
85
|
+
}
|
|
86
|
+
export interface ApplyOptions {
|
|
87
|
+
readonly prune: boolean;
|
|
88
|
+
/** Types whose live list may be short, where prune is refused regardless. */
|
|
89
|
+
readonly pruneUnsafe: ReadonlyArray<string>;
|
|
90
|
+
/** Send fields that cannot be read back, even when nothing else changed. */
|
|
91
|
+
readonly writeOnly: boolean;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The writes one plan item needs, in order.
|
|
95
|
+
*
|
|
96
|
+
* `refIds` carries ids for references this run created, keyed `type/key` — a
|
|
97
|
+
* routing rule planned before its bot existed resolves here, at execution
|
|
98
|
+
* time, with the id the create actually returned.
|
|
99
|
+
*/
|
|
100
|
+
export declare function writesFor(def: ResourceDef, item: PlanItem, refIds: ReadonlyMap<string, string>, opts: ApplyOptions): ReadonlyArray<WriteOp>;
|
|
101
|
+
/**
|
|
102
|
+
* Every credential-looking string in a write response.
|
|
103
|
+
*
|
|
104
|
+
* Two levels deep, because `POST /v1/customer-cards` answers
|
|
105
|
+
* `{ card: {...}, secret: "cc_…" }` — the secret beside the resource rather
|
|
106
|
+
* than inside it. Deeper than that is not swept: a nested config object can
|
|
107
|
+
* legitimately hold `__redacted__` and reporting the sentinel as a secret
|
|
108
|
+
* would be noise.
|
|
109
|
+
*/
|
|
110
|
+
export declare function harvestSecrets(def: ResourceDef, item: PlanItem, response: unknown): ReadonlyArray<FoundSecret>;
|
|
111
|
+
/** The three buckets, as `apply` prints them. */
|
|
112
|
+
export declare function renderOutcomes(outcomes: ReadonlyArray<ApplyOutcome>): string;
|
|
113
|
+
//# sourceMappingURL=apply.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/wac/apply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,OAAO,EAAsC,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAEtF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1C,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;IAC1D,6CAA6C;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;CAC9C;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5C,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAMD;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACnC,IAAI,EAAE,YAAY,GACjB,aAAa,CAAC,OAAO,CAAC,CA4GxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,OAAO,GAChB,aAAa,CAAC,WAAW,CAAC,CA6B5B;AAED,iDAAiD;AACjD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,MAAM,CAiE5E"}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `apply`: turning a plan into writes, and the three decisions that shape it.
|
|
3
|
+
*
|
|
4
|
+
* ═══ 1. PARTIAL FAILURE: CONTINUE, SKIP DEPENDENTS, EXIT NON-ZERO ════════
|
|
5
|
+
*
|
|
6
|
+
* Twelve resources, and the seventh answers 400. Stopping there leaves six
|
|
7
|
+
* applied and five untried; continuing leaves eleven applied and one failed.
|
|
8
|
+
* Both are half-applied — there is no transaction across nine REST
|
|
9
|
+
* collections and pretending otherwise would be a lie — so the choice is
|
|
10
|
+
* about which half-applied state is more useful, and continuing wins for
|
|
11
|
+
* three reasons:
|
|
12
|
+
*
|
|
13
|
+
* - It reports every problem in one run. A document with four bad rows
|
|
14
|
+
* takes one `apply` to find out, not four.
|
|
15
|
+
* - The ones that succeeded are IDEMPOTENT to re-run. Every write carries
|
|
16
|
+
* an `Idempotency-Key` (CONTRACT.md) and, more to the point, the next
|
|
17
|
+
* `plan` recomputes from live state, so a succeeded resource is simply a
|
|
18
|
+
* `noop` next time. Stopping early buys nothing that re-running does not.
|
|
19
|
+
* - Stopping at the seventh tells you nothing about the eighth, and the
|
|
20
|
+
* eighth is frequently the same mistake.
|
|
21
|
+
*
|
|
22
|
+
* The one thing continuing must NOT do is cascade. A routing rule that names
|
|
23
|
+
* a bot whose create just failed is SKIPPED, not attempted: attempting it
|
|
24
|
+
* produces a second, confusing error about a null `botId` that sends the
|
|
25
|
+
* reader to the wrong place. `dependsOn` on each plan item is what makes that
|
|
26
|
+
* possible, and the skip says which failure caused it.
|
|
27
|
+
*
|
|
28
|
+
* Whatever happened, the final output is three explicit buckets — applied,
|
|
29
|
+
* failed, skipped — and the exit code is 1 if either of the last two is
|
|
30
|
+
* non-empty. "Which resources changed" is never left to inference.
|
|
31
|
+
*
|
|
32
|
+
* ═══ 2. SECRETS: PRINTED ONCE, STORED NOWHERE ════════════════════════════
|
|
33
|
+
*
|
|
34
|
+
* Four creates on this surface answer with a credential that no route will
|
|
35
|
+
* ever read back: a bot's `signingSecret` and its `cb_` token, a customer
|
|
36
|
+
* card's `cc_` secret (returned BESIDE the card, not inside it), and a
|
|
37
|
+
* webhook endpoint's `whsec_`. Losing one means delete and recreate.
|
|
38
|
+
*
|
|
39
|
+
* So `apply` prints them, in a block of their own, at the end, where they
|
|
40
|
+
* cannot scroll past unnoticed — and writes them nowhere. Not into the
|
|
41
|
+
* document, not into a state file, not into `~/.config/chatly`. Writing a
|
|
42
|
+
* credential into a file that lives in git is the failure this feature would
|
|
43
|
+
* otherwise industrialise, and the document is the one file we KNOW is in
|
|
44
|
+
* git. There is deliberately no `--save-secrets`.
|
|
45
|
+
*
|
|
46
|
+
* The sweep is belt and braces: the declared `secrets` list per resource,
|
|
47
|
+
* PLUS anything whose field name reads like a credential. A hand-written list
|
|
48
|
+
* goes stale, and CLAUDE.md records twice over what that looks like — a
|
|
49
|
+
* gate's green line counting what it managed to parse rather than what
|
|
50
|
+
* exists. A field called `webhookToken` that nobody told `resources.ts` about
|
|
51
|
+
* is still surfaced.
|
|
52
|
+
*
|
|
53
|
+
* ═══ 3. THE BODY IS BUILT FROM THE DIFF, NOT FROM THE DOCUMENT ═══════════
|
|
54
|
+
*
|
|
55
|
+
* An update sends only the fields that changed, to the route that owns them —
|
|
56
|
+
* which is how a bot's `webhookUrl` reaches `/v1/bots/{id}/external` while
|
|
57
|
+
* its `model` reaches `/v1/bots/{id}`. The exception is a `fullReplace`
|
|
58
|
+
* resource, where a partial body would RESET everything it omits: `triggers`
|
|
59
|
+
* writes every settable field unconditionally, and an email template's PUT
|
|
60
|
+
* takes `{ subject, body }` and nothing else. For those the whole declared
|
|
61
|
+
* row goes over, and `plan` says so on the line so nobody finds out from the
|
|
62
|
+
* result.
|
|
63
|
+
*/
|
|
64
|
+
import { looksLikeCredential, routeForField } from './resources.js';
|
|
65
|
+
function fill(path, id) {
|
|
66
|
+
return path.replace('{id}', encodeURIComponent(id));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The writes one plan item needs, in order.
|
|
70
|
+
*
|
|
71
|
+
* `refIds` carries ids for references this run created, keyed `type/key` — a
|
|
72
|
+
* routing rule planned before its bot existed resolves here, at execution
|
|
73
|
+
* time, with the id the create actually returned.
|
|
74
|
+
*/
|
|
75
|
+
export function writesFor(def, item, refIds, opts) {
|
|
76
|
+
if (item.action === 'orphan') {
|
|
77
|
+
if (!opts.prune || def.remove === null || opts.pruneUnsafe.includes(def.type)) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
return [{ method: 'DELETE', path: fill(def.remove.path, item.liveId ?? '') }];
|
|
81
|
+
}
|
|
82
|
+
if (item.row === undefined || (item.action !== 'create' && item.action !== 'update')) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
/* References: resolved at plan time where possible, here where not. */
|
|
86
|
+
const wired = {};
|
|
87
|
+
for (const [wire, id] of Object.entries(item.resolved)) {
|
|
88
|
+
wired[wire] = id;
|
|
89
|
+
}
|
|
90
|
+
for (const [wire, ref] of Object.entries(item.row.refs)) {
|
|
91
|
+
if (wired[wire] === undefined) {
|
|
92
|
+
const id = refIds.get(`${ref.type}/${ref.key}`);
|
|
93
|
+
if (id !== undefined) {
|
|
94
|
+
wired[wire] = id;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (item.action === 'create') {
|
|
99
|
+
if (def.create === null) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
return [
|
|
103
|
+
{
|
|
104
|
+
method: 'POST',
|
|
105
|
+
path: def.create.path,
|
|
106
|
+
body: { ...item.row.body, ...wired },
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
/* ── An update. ──────────────────────────────────────────────────────── */
|
|
111
|
+
const liveId = item.liveId ?? '';
|
|
112
|
+
if (def.create?.upsert === true) {
|
|
113
|
+
/*
|
|
114
|
+
* `POST /v1/triggers` with the live id in the body. An id the workspace
|
|
115
|
+
* does not hold is a 404, not a create, so the id MUST come from the row
|
|
116
|
+
* this file matched by name — which is exactly what makes a declarative
|
|
117
|
+
* trigger possible at all.
|
|
118
|
+
*/
|
|
119
|
+
return [
|
|
120
|
+
{
|
|
121
|
+
method: 'POST',
|
|
122
|
+
path: def.create.path,
|
|
123
|
+
body: { id: liveId, ...item.row.body, ...wired },
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
/*
|
|
128
|
+
* Which fields go over.
|
|
129
|
+
*
|
|
130
|
+
* `fullReplace` sends every declared field, because a partial body RESETS
|
|
131
|
+
* what it omits — an email template's PUT takes `{ subject, body }` and
|
|
132
|
+
* writes both, and a trigger's upsert writes every settable column
|
|
133
|
+
* unconditionally. Everything else sends only what changed, which keeps a
|
|
134
|
+
* PATCH to a bot's model from touching its guardrails.
|
|
135
|
+
*
|
|
136
|
+
* `--write-only` adds the fields no diff can see. A customer card's
|
|
137
|
+
* `headers` and a docs source's `token` are never returned, so `plan`
|
|
138
|
+
* reports them as unknowable rather than changed — right by default (a diff
|
|
139
|
+
* that says "changed" every run is a diff nobody reads) and wrong when you
|
|
140
|
+
* have just rotated the credential in the file and need it pushed. This is
|
|
141
|
+
* the only way to push them, which is why the plan line names the flag.
|
|
142
|
+
*/
|
|
143
|
+
const wanted = new Set(def.fullReplace === true
|
|
144
|
+
? Object.keys({ ...item.row.body, ...wired }).filter((f) => f !== def.naturalKey)
|
|
145
|
+
: item.changes.map((c) => c.name));
|
|
146
|
+
if (opts.writeOnly) {
|
|
147
|
+
for (const field of def.opaque) {
|
|
148
|
+
if (item.row.body[field] !== undefined) {
|
|
149
|
+
wanted.add(field);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/*
|
|
154
|
+
* Emitted in `def.update` ORDER, not in the order the fields happened to
|
|
155
|
+
* change. Two PATCHes to two routes is already an unavoidable
|
|
156
|
+
* non-atomicity; making their order depend on which key the author listed
|
|
157
|
+
* first would make it unreproducible as well, and a flaky request sequence
|
|
158
|
+
* is a thing nobody can debug from a log.
|
|
159
|
+
*/
|
|
160
|
+
const ops = [];
|
|
161
|
+
for (const route of def.update) {
|
|
162
|
+
const body = {};
|
|
163
|
+
for (const field of wanted) {
|
|
164
|
+
if (routeForField(def, field) !== route) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
body[field] = wired[field] ?? item.row.body[field];
|
|
168
|
+
}
|
|
169
|
+
if (Object.keys(body).length === 0) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
ops.push({ method: route.method, path: fill(route.path, liveId), body });
|
|
173
|
+
}
|
|
174
|
+
return ops;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Every credential-looking string in a write response.
|
|
178
|
+
*
|
|
179
|
+
* Two levels deep, because `POST /v1/customer-cards` answers
|
|
180
|
+
* `{ card: {...}, secret: "cc_…" }` — the secret beside the resource rather
|
|
181
|
+
* than inside it. Deeper than that is not swept: a nested config object can
|
|
182
|
+
* legitimately hold `__redacted__` and reporting the sentinel as a secret
|
|
183
|
+
* would be noise.
|
|
184
|
+
*/
|
|
185
|
+
export function harvestSecrets(def, item, response) {
|
|
186
|
+
const out = [];
|
|
187
|
+
const seen = new Set();
|
|
188
|
+
const scan = (value, depth) => {
|
|
189
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
for (const [field, raw] of Object.entries(value)) {
|
|
193
|
+
if (typeof raw === 'string') {
|
|
194
|
+
if (raw.length > 0 &&
|
|
195
|
+
raw !== '__redacted__' &&
|
|
196
|
+
(def.secrets.includes(field) || looksLikeCredential(field)) &&
|
|
197
|
+
!seen.has(`${field}:${raw}`)) {
|
|
198
|
+
seen.add(`${field}:${raw}`);
|
|
199
|
+
out.push({ type: def.type, key: item.key, field, value: raw });
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (depth < 1) {
|
|
204
|
+
scan(raw, depth + 1);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
scan(response, 0);
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
/** The three buckets, as `apply` prints them. */
|
|
212
|
+
export function renderOutcomes(outcomes) {
|
|
213
|
+
const applied = outcomes.filter((o) => o.state === 'applied');
|
|
214
|
+
const failed = outcomes.filter((o) => o.state === 'failed');
|
|
215
|
+
const skipped = outcomes.filter((o) => o.state === 'skipped');
|
|
216
|
+
const kept = outcomes.filter((o) => o.state === 'kept');
|
|
217
|
+
const lines = [];
|
|
218
|
+
const verb = (o) => o.action === 'orphan' ? 'deleted' : o.action === 'create' ? 'created' : 'changed';
|
|
219
|
+
if (applied.length > 0) {
|
|
220
|
+
lines.push(`Applied (${String(applied.length)})`);
|
|
221
|
+
for (const o of applied) {
|
|
222
|
+
lines.push(` ✓ ${o.type}/${o.key} ${verb(o)}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (failed.length > 0) {
|
|
226
|
+
lines.push('', `Failed (${String(failed.length)}) — nothing was written for these`);
|
|
227
|
+
for (const o of failed) {
|
|
228
|
+
lines.push(` ✗ ${o.type}/${o.key}`);
|
|
229
|
+
lines.push(` ${o.detail ?? 'no detail recorded, which is itself a bug.'}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (skipped.length > 0) {
|
|
233
|
+
lines.push('', `Skipped (${String(skipped.length)}) — not attempted`);
|
|
234
|
+
for (const o of skipped) {
|
|
235
|
+
lines.push(` · ${o.type}/${o.key}`);
|
|
236
|
+
lines.push(` ${o.detail ?? 'no detail recorded, which is itself a bug.'}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (kept.length > 0) {
|
|
240
|
+
lines.push('', `Left alone (${String(kept.length)}) — on the server, not in the document`);
|
|
241
|
+
for (const o of kept) {
|
|
242
|
+
lines.push(` · ${o.type}/${o.key}${o.detail === undefined ? '' : ` ${o.detail}`}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const secrets = outcomes.flatMap((o) => o.secrets);
|
|
246
|
+
if (secrets.length > 0) {
|
|
247
|
+
/*
|
|
248
|
+
* Last, and loud. These are shown exactly once by the API and this is the
|
|
249
|
+
* only moment they exist anywhere outside the database. The banner says
|
|
250
|
+
* what to do rather than merely that they are secret, because "copy it
|
|
251
|
+
* now" is the actionable part and the recovery (delete and recreate) is
|
|
252
|
+
* worse than it sounds for a webhook endpoint.
|
|
253
|
+
*/
|
|
254
|
+
lines.push('', '─── SHOWN ONCE ────────────────────────────────────────────────────', 'The API returns each of these exactly once and no route reads them', 'back. Copy them into your secret store NOW. They are not written to', 'the document, to a state file, or to disk anywhere — losing one means', 'deleting and recreating the resource.', '');
|
|
255
|
+
for (const s of secrets) {
|
|
256
|
+
lines.push(` ${s.type}/${s.key} ${s.field} = ${s.value}`);
|
|
257
|
+
}
|
|
258
|
+
lines.push('───────────────────────────────────────────────────────────────────');
|
|
259
|
+
}
|
|
260
|
+
if (lines.length === 0) {
|
|
261
|
+
return 'Nothing to do — the workspace already matches the document.\n';
|
|
262
|
+
}
|
|
263
|
+
return `${lines.join('\n')}\n`;
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=apply.js.map
|