@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.
Files changed (73) hide show
  1. package/dist/commands/ai.d.ts +4 -0
  2. package/dist/commands/ai.d.ts.map +1 -0
  3. package/dist/commands/ai.js +149 -0
  4. package/dist/commands/ai.js.map +1 -0
  5. package/dist/commands/common.d.ts +79 -0
  6. package/dist/commands/common.d.ts.map +1 -0
  7. package/dist/commands/common.js +194 -0
  8. package/dist/commands/common.js.map +1 -0
  9. package/dist/commands/config.d.ts +40 -0
  10. package/dist/commands/config.d.ts.map +1 -0
  11. package/dist/commands/config.js +336 -0
  12. package/dist/commands/config.js.map +1 -0
  13. package/dist/commands/insights.d.ts +5 -0
  14. package/dist/commands/insights.d.ts.map +1 -0
  15. package/dist/commands/insights.js +277 -0
  16. package/dist/commands/insights.js.map +1 -0
  17. package/dist/commands/integrations.d.ts +15 -0
  18. package/dist/commands/integrations.d.ts.map +1 -0
  19. package/dist/commands/integrations.js +256 -0
  20. package/dist/commands/integrations.js.map +1 -0
  21. package/dist/commands/resources.d.ts +19 -7
  22. package/dist/commands/resources.d.ts.map +1 -1
  23. package/dist/commands/resources.js +100 -191
  24. package/dist/commands/resources.js.map +1 -1
  25. package/dist/commands/workspace-as-code.d.ts +27 -0
  26. package/dist/commands/workspace-as-code.d.ts.map +1 -0
  27. package/dist/commands/workspace-as-code.js +377 -0
  28. package/dist/commands/workspace-as-code.js.map +1 -0
  29. package/dist/help.d.ts +25 -1
  30. package/dist/help.d.ts.map +1 -1
  31. package/dist/help.js +104 -8
  32. package/dist/help.js.map +1 -1
  33. package/dist/run.d.ts +17 -0
  34. package/dist/run.d.ts.map +1 -1
  35. package/dist/run.js +88 -41
  36. package/dist/run.js.map +1 -1
  37. package/dist/wac/__tests__/wac-fixtures.d.ts +20 -0
  38. package/dist/wac/__tests__/wac-fixtures.d.ts.map +1 -0
  39. package/dist/wac/__tests__/wac-fixtures.js +38 -0
  40. package/dist/wac/__tests__/wac-fixtures.js.map +1 -0
  41. package/dist/wac/apply.d.ts +113 -0
  42. package/dist/wac/apply.d.ts.map +1 -0
  43. package/dist/wac/apply.js +265 -0
  44. package/dist/wac/apply.js.map +1 -0
  45. package/dist/wac/diff.d.ts +137 -0
  46. package/dist/wac/diff.d.ts.map +1 -0
  47. package/dist/wac/diff.js +413 -0
  48. package/dist/wac/diff.js.map +1 -0
  49. package/dist/wac/document.d.ts +110 -0
  50. package/dist/wac/document.d.ts.map +1 -0
  51. package/dist/wac/document.js +429 -0
  52. package/dist/wac/document.js.map +1 -0
  53. package/dist/wac/render.d.ts +52 -0
  54. package/dist/wac/render.d.ts.map +1 -0
  55. package/dist/wac/render.js +156 -0
  56. package/dist/wac/render.js.map +1 -0
  57. package/dist/wac/resource-def.d.ts +116 -0
  58. package/dist/wac/resource-def.d.ts.map +1 -0
  59. package/dist/wac/resource-def.js +44 -0
  60. package/dist/wac/resource-def.js.map +1 -0
  61. package/dist/wac/resources.d.ts +104 -0
  62. package/dist/wac/resources.d.ts.map +1 -0
  63. package/dist/wac/resources.js +541 -0
  64. package/dist/wac/resources.js.map +1 -0
  65. package/dist/wac/values.d.ts +16 -0
  66. package/dist/wac/values.d.ts.map +1 -0
  67. package/dist/wac/values.js +53 -0
  68. package/dist/wac/values.js.map +1 -0
  69. package/package.json +3 -2
  70. package/dist/cli.spec.d.ts +0 -2
  71. package/dist/cli.spec.d.ts.map +0 -1
  72. package/dist/cli.spec.js +0 -226
  73. 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.3';
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
- const json = bool(args.flags, 'json');
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 has.'
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":"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"}
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