@intercom/cli 0.8.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/index.js +430 -48
  3. package/package.json +1 -5
package/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  View it from the CLI with `intercom changelog`.
9
9
 
10
+ ## [0.10.0] - 2026-08-17
11
+
12
+ ### Added
13
+
14
+ - `fin procedures publish <id>` sets a procedure's state to live, and `fin procedures pause <id>` sets it to paused — a convenience for download, edit the state, and re-upload. Both accept `--note <text>` to label the save in `fin procedures versions <id>`. There is no `draft` verb: the upload endpoint rejects that state and directs you to `pause`.
15
+ - `fin procedures versions <id>` lists a procedure's server-side version history, showing which version is live, which is draft, and who saved it. `fin procedures get <id> --version <version_id>` downloads any of those versions as re-uploadable YAML, so a local file can be diffed against what Fin is actually running.
16
+ - `fin procedures upload <file> --note <text>` records a version note against the save, so the entry is identifiable in `fin procedures versions`. `--verify` compares what the server saved against the file you uploaded and exits non-zero listing any field that did not survive the round-trip (fields the server adds, like ids and defaults, are ignored) — useful when a setting silently resets on push.
17
+ - `fin attributes get <name>` shows a single Fin custom attribute by name. `fin attributes download <file>` exports all attributes to a YAML file in the shape `fin attributes upload` accepts, completing the export/edit/re-import round-trip (attributes previously supported only `list` and `upload`).
18
+
19
+ ### Changed
20
+
21
+ - `--dry-run` (`-n`) now prints the actual file payload for upload commands (`fin attributes upload`, `fin procedures upload`, `fin simulations upload`, `fin workflows upload`) instead of an opaque `[FormData]`. You can now preview the exact YAML that would be sent — including the routing `type:` the CLI adds for you — before uploading.
22
+
23
+ ## [0.9.0] - 2026-07-03
24
+
25
+ ### Added
26
+
27
+ - `fin attributes upload <file>` creates or updates Fin custom attributes from a YAML file. Attributes were previously read-only (`list`). The file can be a list of attributes, a single attribute, or the full `type: attribute` / `attributes:` server shape — the CLI normalizes it and sets the routing type for you. Each attribute needs a dotted `name` (e.g. `user.order_status`) and a data `type` (`string`, `integer`, `decimal`, `float`, `boolean`, `list`, `date`, `object`, or `array`).
28
+ - `fin procedures get <id> [output] --with-simulations` downloads a procedure's simulations alongside it. With the flag, `[output]` is a directory (default: current directory) and the CLI writes both `procedure.yaml` and `simulations.yaml`. Without it, `[output]` is a single file for the procedure YAML (previously `get` could only print to stdout).
29
+ - `fin simulations upload <file>` re-uploads a simulation set (the `simulations.yaml` written by `fin procedures get --with-simulations`, or `fin simulations list` output), completing the export/edit/re-import round-trip for a procedure's tests.
30
+
10
31
  ## [0.8.3] - 2026-06-18
11
32
 
12
33
  ### Fixed
package/dist/index.js CHANGED
@@ -84,6 +84,9 @@ var _dryRun = false;
84
84
  function setDryRun(enabled) {
85
85
  _dryRun = enabled;
86
86
  }
87
+ function isDryRun() {
88
+ return _dryRun;
89
+ }
87
90
  var IntercomClient = class _IntercomClient {
88
91
  token;
89
92
  authType;
@@ -153,10 +156,18 @@ var IntercomClient = class _IntercomClient {
153
156
  const url = `${this.baseUrl}${path}`;
154
157
  if (_dryRun) {
155
158
  console.error(`> ${method} ${url}`);
156
- if (body) {
157
- console.error(
158
- `> Body: ${body instanceof FormData ? "[FormData]" : JSON.stringify(body)}`
159
- );
159
+ if (body instanceof FormData) {
160
+ for (const [name, value] of body.entries()) {
161
+ if (typeof value === "string") {
162
+ console.error(`> ${name}: ${value}`);
163
+ } else {
164
+ const fileName = "name" in value ? value.name : "file";
165
+ console.error(`> ${name} (${fileName}):
166
+ ${await value.text()}`);
167
+ }
168
+ }
169
+ } else if (body) {
170
+ console.error(`> Body: ${JSON.stringify(body)}`);
160
171
  }
161
172
  return {};
162
173
  }
@@ -2569,9 +2580,49 @@ Examples:
2569
2580
  import { writeFile as writeFile3 } from "fs/promises";
2570
2581
 
2571
2582
  // src/commands/fin/procedures.ts
2572
- import { readFile as readFile2 } from "fs/promises";
2573
- import { basename } from "path";
2583
+ import { readFile as readFile2, mkdir as mkdir2 } from "fs/promises";
2584
+ import { basename, join as join3 } from "path";
2574
2585
  import AdmZip from "adm-zip";
2586
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
2587
+
2588
+ // src/utils/yaml-drift.ts
2589
+ function isRecord(value) {
2590
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2591
+ }
2592
+ function scalarsMatch(sent, saved) {
2593
+ if (sent == null && saved == null) {
2594
+ return true;
2595
+ }
2596
+ if (sent == null || saved == null) {
2597
+ return false;
2598
+ }
2599
+ if (typeof saved === "object") {
2600
+ return false;
2601
+ }
2602
+ return String(sent) === String(saved);
2603
+ }
2604
+ function findDrift(sent, saved, path = "") {
2605
+ const here = path || ".";
2606
+ if (Array.isArray(sent)) {
2607
+ if (!Array.isArray(saved) || saved.length !== sent.length) {
2608
+ return [here];
2609
+ }
2610
+ return sent.flatMap((item, index) => findDrift(item, saved[index], `${path}[${index}]`));
2611
+ }
2612
+ if (isRecord(sent)) {
2613
+ if (!isRecord(saved)) {
2614
+ return [here];
2615
+ }
2616
+ return Object.entries(sent).flatMap(
2617
+ ([key, value]) => findDrift(value, saved[key], path ? `${path}.${key}` : key)
2618
+ );
2619
+ }
2620
+ return scalarsMatch(sent, saved) ? [] : [here];
2621
+ }
2622
+
2623
+ // src/commands/fin/procedures.ts
2624
+ var STATE_LIVE = "live";
2625
+ var STATE_PAUSED = "paused";
2575
2626
  function registerProceduresCommand(fin) {
2576
2627
  const procedures = fin.command("procedures").description("Manage Fin SDK procedures");
2577
2628
  procedures.command("list").description("List procedures from the Fin SDK manifest").option("--json", "Output as JSON").option("--verbose", "Show HTTP request/response details").action(async function() {
@@ -2591,27 +2642,212 @@ function registerProceduresCommand(fin) {
2591
2642
  return table({ columns: ["ID", "NAME"], rows });
2592
2643
  }, items);
2593
2644
  });
2594
- procedures.command("get <id>").description("Download a procedure by ID and print its YAML content").option("--verbose", "Show HTTP request/response details").action(async function(id) {
2645
+ procedures.command("get <id> [output]").description("Download a procedure by ID as YAML (writes to [output], or stdout)").option("--with-simulations", "Also download the procedure's simulations").option("--version <version_id>", "Download a historical version instead of the current one").option("--verbose", "Show HTTP request/response details").addHelpText(
2646
+ "after",
2647
+ `
2648
+ Examples:
2649
+ $ intercom fin procedures get proc-1 # print YAML to stdout
2650
+ $ intercom fin procedures get proc-1 procedure.yaml # write YAML to a file
2651
+ $ intercom fin procedures get proc-1 ./out --with-simulations
2652
+ # writes ./out/procedure.yaml and ./out/simulations.yaml
2653
+ $ intercom fin procedures versions proc-1 # find a version ID
2654
+ $ intercom fin procedures get proc-1 --version 4821 # print that version's YAML
2655
+
2656
+ With --with-simulations, [output] is treated as a directory (default: current
2657
+ directory). Without it, [output] is a single file for the procedure YAML.
2658
+
2659
+ --version downloads the re-uploadable YAML of a historical version, so you can
2660
+ diff a local file against what is live (\`intercom fin procedures versions <id>\`
2661
+ shows which version that is). Simulations are not versioned, so --version and
2662
+ --with-simulations cannot be combined.
2663
+
2664
+ Re-import the pieces with \`intercom fin procedures upload <procedure.yaml>\`
2665
+ and \`intercom fin simulations upload <simulations.yaml>\`.`
2666
+ ).action(async function(id, output) {
2595
2667
  const opts = this.optsWithGlobals();
2668
+ if (opts.version && opts.withSimulations) {
2669
+ throw new CLIError("--version cannot be combined with --with-simulations (simulations are not versioned)");
2670
+ }
2596
2671
  const client = await resolveClient(opts);
2597
- const buffer = await client.getBuffer(`/fin-sdk/procedures/${id}/download`);
2598
- const zip = new AdmZip(buffer);
2599
- const entry = zip.getEntries().find((e) => e.entryName.endsWith("procedure.yaml"));
2600
- if (!entry) {
2601
- throw new Error("procedure.yaml not found in downloaded ZIP");
2672
+ const procedureYaml = opts.version ? await getVersionYaml(client, id, opts.version) : await getCurrentYaml(client, id);
2673
+ let simulationsYaml;
2674
+ if (opts.withSimulations) {
2675
+ simulationsYaml = await client.getText(`/fin-sdk/procedures/${id}/simulations`);
2676
+ }
2677
+ if (opts.withSimulations) {
2678
+ const dir = output ?? ".";
2679
+ await mkdir2(dir, { recursive: true });
2680
+ const procedurePath = join3(dir, "procedure.yaml");
2681
+ const simulationsPath = join3(dir, "simulations.yaml");
2682
+ await atomicWriteFile(procedurePath, procedureYaml, 420);
2683
+ await atomicWriteFile(simulationsPath, simulationsYaml ?? "", 420);
2684
+ console.error(`Wrote procedure ${id} to ${procedurePath}`);
2685
+ console.error(`Wrote simulations to ${simulationsPath}`);
2686
+ return;
2687
+ }
2688
+ if (output) {
2689
+ await atomicWriteFile(output, procedureYaml, 420);
2690
+ console.error(`Wrote procedure ${id} to ${output}`);
2691
+ } else {
2692
+ console.log(procedureYaml);
2602
2693
  }
2603
- console.log(zip.readAsText(entry));
2604
2694
  });
2605
- procedures.command("upload <file>").description("Upload a procedure file to Fin SDK").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").action(async function(filePath) {
2695
+ procedures.command("upload <file>").description("Upload a procedure file to Fin SDK").option("--note <text>", "Version note to record against this save").option("--verify", "Fail if the saved procedure differs from the file uploaded").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
2696
+ "after",
2697
+ `
2698
+ Examples:
2699
+ $ intercom fin procedures upload procedure.yaml
2700
+ $ intercom fin procedures upload procedure.yaml --note "tightened refund wording"
2701
+ $ intercom fin procedures upload procedure.yaml --verify
2702
+
2703
+ --note labels this save in the procedure's version history, so it is
2704
+ identifiable in \`intercom fin procedures versions <id>\` instead of appearing
2705
+ as an unlabelled entry.
2706
+
2707
+ --verify compares the procedure the server saved against the file just uploaded
2708
+ and exits non-zero listing any field that did not survive the round-trip. Fields
2709
+ the server adds (ids, defaults) are ignored; only content that was sent and came
2710
+ back missing or changed is reported.`
2711
+ ).action(async function(filePath) {
2606
2712
  const opts = this.optsWithGlobals();
2607
2713
  const client = await resolveClient(opts);
2608
2714
  const fileContent = await readFile2(filePath);
2609
2715
  const formData = new FormData();
2610
2716
  const blob = new Blob([fileContent], { type: "application/x-yaml" });
2611
2717
  formData.append("file_data", blob, basename(filePath));
2718
+ if (opts.note) {
2719
+ formData.append("version_note", opts.note);
2720
+ }
2612
2721
  const result = await client.postFormData("/fin-sdk/upload", formData);
2613
2722
  render(result, opts, () => formatJson(result));
2723
+ if (opts.verify) {
2724
+ verifySaved(filePath, fileContent.toString("utf8"), result);
2725
+ }
2614
2726
  });
2727
+ procedures.command("publish <id>").description("Set a procedure's state to live").option("--note <text>", "Version note to record against this save").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
2728
+ "after",
2729
+ `
2730
+ Examples:
2731
+ $ intercom fin procedures publish proc-1
2732
+ $ intercom fin procedures publish proc-1 --note "tightened refund wording"
2733
+
2734
+ This is a convenience for download \u2192 set state \u2192 upload. If another operator
2735
+ edits the procedure between the download and upload, those edits will be
2736
+ overwritten (the server does not expose a concurrency token).
2737
+
2738
+ --note labels this save in \`intercom fin procedures versions <id>\`.`
2739
+ ).action(async function(id) {
2740
+ const opts = this.optsWithGlobals();
2741
+ await setProcedureState(id, STATE_LIVE, opts);
2742
+ });
2743
+ procedures.command("pause <id>").description("Set a procedure's state to paused").option("--note <text>", "Version note to record against this save").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
2744
+ "after",
2745
+ `
2746
+ Examples:
2747
+ $ intercom fin procedures pause proc-1
2748
+ $ intercom fin procedures pause proc-1 --note "pausing during refund policy review"
2749
+
2750
+ This is a convenience for download \u2192 set state \u2192 upload. If another operator
2751
+ edits the procedure between the download and upload, those edits will be
2752
+ overwritten (the server does not expose a concurrency token).
2753
+
2754
+ pause is the way to deactivate a procedure: the server rejects an uploaded
2755
+ \`state: draft\` outright, so there is no separate "unpublish to draft" command.
2756
+
2757
+ --note labels this save in \`intercom fin procedures versions <id>\`.`
2758
+ ).action(async function(id) {
2759
+ const opts = this.optsWithGlobals();
2760
+ await setProcedureState(id, STATE_PAUSED, opts);
2761
+ });
2762
+ procedures.command("versions <id>").description("List a procedure's server-side version history (which one is live)").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
2763
+ "after",
2764
+ `
2765
+ Examples:
2766
+ $ intercom fin procedures versions proc-1
2767
+ $ intercom fin procedures versions proc-1 --jq '.entity_version_id'
2768
+
2769
+ STATUS is the server's own vocabulary: live is what Fin is running now, draft is
2770
+ unpublished, previously_live is superseded. Download any of them with
2771
+ \`intercom fin procedures get <id> --version <VERSION>\`.`
2772
+ ).action(async function(id) {
2773
+ const opts = this.optsWithGlobals();
2774
+ const client = await resolveClient(opts);
2775
+ const result = await client.get(`/fin-sdk/procedures/${id}/versions`);
2776
+ const items = result.versions ?? [];
2777
+ render(items, opts, () => {
2778
+ if (items.length === 0) {
2779
+ return "No versions found.";
2780
+ }
2781
+ const rows = items.map((v) => ({
2782
+ VERSION: String(v.entity_version_id ?? ""),
2783
+ STATUS: String(v.status ?? ""),
2784
+ CREATED: String(v.created_at ?? ""),
2785
+ AUTHOR: String(v.author?.name ?? ""),
2786
+ NOTE: String(v.note ?? "")
2787
+ }));
2788
+ return table({ columns: ["VERSION", "STATUS", "CREATED", "AUTHOR", "NOTE"], rows });
2789
+ }, items);
2790
+ });
2791
+ }
2792
+ async function setProcedureState(id, state, opts) {
2793
+ const client = await resolveClient(opts);
2794
+ if (isDryRun()) {
2795
+ console.error(`> would set procedure ${id} state to ${state}`);
2796
+ return;
2797
+ }
2798
+ const procedureYaml = await getCurrentYaml(client, id);
2799
+ let parsed;
2800
+ try {
2801
+ parsed = parseYaml(procedureYaml);
2802
+ } catch (err) {
2803
+ throw new CLIError(
2804
+ `server returned unparseable YAML for procedure ${id}: ${err.message}`
2805
+ );
2806
+ }
2807
+ parsed.state = state;
2808
+ const formData = new FormData();
2809
+ const blob = new Blob([stringifyYaml(parsed)], { type: "application/x-yaml" });
2810
+ formData.append("file_data", blob, `procedure-${id}.yaml`);
2811
+ if (opts.note) {
2812
+ formData.append("version_note", opts.note);
2813
+ }
2814
+ const result = await client.postFormData("/fin-sdk/upload", formData);
2815
+ render(result, opts, () => formatJson(result));
2816
+ }
2817
+ async function getCurrentYaml(client, id) {
2818
+ const buffer = await client.getBuffer(`/fin-sdk/procedures/${id}/download`);
2819
+ const zip = new AdmZip(buffer);
2820
+ const entry = zip.getEntries().find((e) => e.entryName.endsWith("procedure.yaml"));
2821
+ if (!entry) {
2822
+ throw new CLIError("procedure.yaml not found in downloaded ZIP");
2823
+ }
2824
+ return zip.readAsText(entry);
2825
+ }
2826
+ async function getVersionYaml(client, id, versionId) {
2827
+ const result = await client.get(
2828
+ `/fin-sdk/procedures/${id}/versions/${versionId}`
2829
+ );
2830
+ const content = result.content;
2831
+ if (content == null) {
2832
+ throw new CLIError(`Procedure version ${versionId} returned no content`);
2833
+ }
2834
+ return typeof content === "string" ? content : stringifyYaml(content);
2835
+ }
2836
+ function verifySaved(filePath, sentYaml, result) {
2837
+ if (typeof result.new_file_content !== "string") {
2838
+ throw new CLIError("Cannot verify: the server did not return the saved procedure");
2839
+ }
2840
+ const drift = findDrift(parseYaml(sentYaml), parseYaml(result.new_file_content));
2841
+ if (drift.length === 0) {
2842
+ console.error(`Verified: the saved procedure matches ${filePath}`);
2843
+ return;
2844
+ }
2845
+ for (const path of drift) {
2846
+ console.error(` ${path}`);
2847
+ }
2848
+ throw new CLIError(
2849
+ `${drift.length} field(s) did not survive the upload (listed above). Compare with \`intercom fin procedures get ${result.id ?? "<id>"}\`.`
2850
+ );
2615
2851
  }
2616
2852
 
2617
2853
  // src/commands/fin/data-connectors.ts
@@ -2766,13 +3002,39 @@ Optional fields: state (live or paused, default: paused), audiences`
2766
3002
  }
2767
3003
 
2768
3004
  // src/commands/fin/attributes.ts
3005
+ import { readFile as readFile3 } from "fs/promises";
3006
+ import { basename as basename2 } from "path";
3007
+ import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
3008
+ var ATTRIBUTE_TYPE = "attribute";
3009
+ async function fetchAttributes(client) {
3010
+ const result = await client.get("/fin-sdk/attributes");
3011
+ return Array.isArray(result) ? result : result.attributes ?? [];
3012
+ }
3013
+ function buildAttributesUploadBody(raw) {
3014
+ let parsed;
3015
+ try {
3016
+ parsed = parseYaml2(raw);
3017
+ } catch {
3018
+ return raw;
3019
+ }
3020
+ if (Array.isArray(parsed)) {
3021
+ return stringifyYaml2({ type: ATTRIBUTE_TYPE, attributes: parsed });
3022
+ }
3023
+ if (parsed == null || typeof parsed !== "object") {
3024
+ return raw;
3025
+ }
3026
+ const obj = parsed;
3027
+ if (Array.isArray(obj.attributes)) {
3028
+ return stringifyYaml2({ ...obj, type: ATTRIBUTE_TYPE });
3029
+ }
3030
+ return stringifyYaml2({ type: ATTRIBUTE_TYPE, attributes: [obj] });
3031
+ }
2769
3032
  function registerAttributesCommand(fin) {
2770
3033
  const attributes = fin.command("attributes").description("Manage Fin SDK attributes");
2771
3034
  attributes.command("list").description("List Fin SDK attributes").option("--json", "Output as JSON").option("--verbose", "Show HTTP request/response details").action(async function() {
2772
3035
  const opts = this.optsWithGlobals();
2773
3036
  const client = await resolveClient(opts);
2774
- const result = await client.get("/fin-sdk/attributes");
2775
- const items = Array.isArray(result) ? result : result.attributes ?? [];
3037
+ const items = await fetchAttributes(client);
2776
3038
  render(items, opts, () => {
2777
3039
  if (items.length === 0) {
2778
3040
  return "No attributes found.";
@@ -2785,6 +3047,75 @@ function registerAttributesCommand(fin) {
2785
3047
  return table({ columns: ["NAME", "TYPE", "DESCRIPTION"], rows });
2786
3048
  }, items);
2787
3049
  });
3050
+ attributes.command("get <name>").description("Show a single Fin SDK attribute by name").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").action(async function(name) {
3051
+ const opts = this.optsWithGlobals();
3052
+ const client = await resolveClient(opts);
3053
+ const items = await fetchAttributes(client);
3054
+ const match = items.find((item) => item.name === name);
3055
+ if (!match) {
3056
+ throw new NotFoundError(`No Fin attribute named "${name}".`);
3057
+ }
3058
+ render(
3059
+ match,
3060
+ opts,
3061
+ () => detail({
3062
+ "Name": match.name != null ? String(match.name) : void 0,
3063
+ "Type": match.type != null ? String(match.type) : void 0,
3064
+ "Description": match.description != null ? String(match.description) : void 0
3065
+ })
3066
+ );
3067
+ });
3068
+ attributes.command("download <file>").description("Download all Fin SDK attributes to a YAML file").option("--verbose", "Show HTTP request/response details").action(async function(filePath) {
3069
+ const opts = this.optsWithGlobals();
3070
+ const client = await resolveClient(opts);
3071
+ const items = await fetchAttributes(client);
3072
+ const yaml = stringifyYaml2({ type: ATTRIBUTE_TYPE, attributes: items });
3073
+ await atomicWriteFile(filePath, yaml, 420);
3074
+ console.error(`Wrote ${items.length} attribute${items.length === 1 ? "" : "s"} to ${filePath}`);
3075
+ });
3076
+ attributes.command("upload <file>").description("Upload attributes from a YAML file (creates or updates)").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
3077
+ "after",
3078
+ `
3079
+ The file holds one or more attributes. The CLI wraps them in the bulk shape the
3080
+ server expects and sets the routing type for you, so any of these work:
3081
+
3082
+ # a list of attributes
3083
+ - name: user.order_status # name must contain a dot
3084
+ type: string # data type: string, integer, decimal, float,
3085
+ description: Order state # boolean, list, date, object, or array
3086
+
3087
+ # ...or a single attribute
3088
+ name: company.tier
3089
+ type: string
3090
+
3091
+ # ...or the full server shape
3092
+ type: attribute
3093
+ attributes:
3094
+ - name: user.order_status
3095
+ type: string
3096
+
3097
+ Examples:
3098
+ $ intercom fin attributes upload ./attributes.yaml
3099
+ $ intercom fin attributes upload ./attributes.yaml --json`
3100
+ ).action(async function(filePath) {
3101
+ const opts = this.optsWithGlobals();
3102
+ const client = await resolveClient(opts);
3103
+ const fileContent = await readFile3(filePath, "utf8");
3104
+ const yamlBody = buildAttributesUploadBody(fileContent);
3105
+ const formData = new FormData();
3106
+ const blob = new Blob([yamlBody], { type: "application/x-yaml" });
3107
+ formData.append("file_data", blob, basename2(filePath));
3108
+ const result = await client.postFormData("/fin-sdk/upload", formData);
3109
+ render(
3110
+ result,
3111
+ opts,
3112
+ () => detail({
3113
+ "ID": result.id != null ? String(result.id) : void 0,
3114
+ "Type": result.type != null ? String(result.type) : void 0,
3115
+ "Message": result.message != null ? String(result.message) : void 0
3116
+ })
3117
+ );
3118
+ });
2788
3119
  }
2789
3120
 
2790
3121
  // src/commands/fin/audiences.ts
@@ -2809,16 +3140,16 @@ function registerAudiencesCommand(fin) {
2809
3140
  }
2810
3141
 
2811
3142
  // src/commands/fin/workflows.ts
2812
- import { readFile as readFile3 } from "fs/promises";
2813
- import { basename as basename2 } from "path";
2814
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
2815
- var STATE_LIVE = "live";
2816
- var STATE_PAUSED = "paused";
3143
+ import { readFile as readFile4 } from "fs/promises";
3144
+ import { basename as basename3 } from "path";
3145
+ import { parse as parseYaml3, stringify as stringifyYaml3 } from "yaml";
3146
+ var STATE_LIVE2 = "live";
3147
+ var STATE_PAUSED2 = "paused";
2817
3148
  var WORKFLOW_TYPE = "workflow";
2818
3149
  function ensureWorkflowType(raw) {
2819
3150
  let parsed;
2820
3151
  try {
2821
- parsed = parseYaml(raw);
3152
+ parsed = parseYaml3(raw);
2822
3153
  } catch {
2823
3154
  return raw;
2824
3155
  }
@@ -2830,7 +3161,7 @@ function ensureWorkflowType(raw) {
2830
3161
  return raw;
2831
3162
  }
2832
3163
  obj.type = WORKFLOW_TYPE;
2833
- return stringifyYaml(obj);
3164
+ return stringifyYaml3(obj);
2834
3165
  }
2835
3166
  function registerWorkflowsCommand(fin) {
2836
3167
  const workflows = fin.command("workflows").description("Manage Fin SDK workflows");
@@ -2867,7 +3198,7 @@ The YAML it prints is ready to edit and re-upload with
2867
3198
  const client = await resolveClient(opts);
2868
3199
  const text = await client.getText(`/fin-sdk/workflows/${id}`);
2869
3200
  if (opts.json || opts.jq || opts.agent) {
2870
- const parsed = parseYaml(text);
3201
+ const parsed = parseYaml3(text);
2871
3202
  render(parsed, opts, () => formatJson(parsed));
2872
3203
  return;
2873
3204
  }
@@ -2892,11 +3223,11 @@ The YAML file's id field determines whether this creates or updates:
2892
3223
  ).action(async function(filePath) {
2893
3224
  const opts = this.optsWithGlobals();
2894
3225
  const client = await resolveClient(opts);
2895
- const fileContent = await readFile3(filePath, "utf8");
3226
+ const fileContent = await readFile4(filePath, "utf8");
2896
3227
  const yamlBody = ensureWorkflowType(fileContent);
2897
3228
  const formData = new FormData();
2898
3229
  const blob = new Blob([yamlBody], { type: "application/x-yaml" });
2899
- formData.append("file_data", blob, basename2(filePath));
3230
+ formData.append("file_data", blob, basename3(filePath));
2900
3231
  const result = await client.postFormData("/fin-sdk/upload", formData);
2901
3232
  render(
2902
3233
  result,
@@ -2925,13 +3256,13 @@ overwritten (the server does not expose a concurrency token).`
2925
3256
  const text = await client.getText(`/fin-sdk/workflows/${id}`);
2926
3257
  let parsed;
2927
3258
  try {
2928
- parsed = parseYaml(text);
3259
+ parsed = parseYaml3(text);
2929
3260
  } catch (err) {
2930
3261
  throw new Error(`server returned unparseable YAML for workflow ${id}: ${err.message}`);
2931
3262
  }
2932
3263
  parsed.type = WORKFLOW_TYPE;
2933
- parsed.state = STATE_LIVE;
2934
- const yamlBody = stringifyYaml(parsed);
3264
+ parsed.state = STATE_LIVE2;
3265
+ const yamlBody = stringifyYaml3(parsed);
2935
3266
  const formData = new FormData();
2936
3267
  const blob = new Blob([yamlBody], { type: "application/x-yaml" });
2937
3268
  formData.append("file_data", blob, `workflow-${id}.yaml`);
@@ -2962,13 +3293,13 @@ overwritten (the server does not expose a concurrency token).`
2962
3293
  const text = await client.getText(`/fin-sdk/workflows/${id}`);
2963
3294
  let parsed;
2964
3295
  try {
2965
- parsed = parseYaml(text);
3296
+ parsed = parseYaml3(text);
2966
3297
  } catch (err) {
2967
3298
  throw new Error(`server returned unparseable YAML for workflow ${id}: ${err.message}`);
2968
3299
  }
2969
3300
  parsed.type = WORKFLOW_TYPE;
2970
- parsed.state = STATE_PAUSED;
2971
- const yamlBody = stringifyYaml(parsed);
3301
+ parsed.state = STATE_PAUSED2;
3302
+ const yamlBody = stringifyYaml3(parsed);
2972
3303
  const formData = new FormData();
2973
3304
  const blob = new Blob([yamlBody], { type: "application/x-yaml" });
2974
3305
  formData.append("file_data", blob, `workflow-${id}.yaml`);
@@ -2986,6 +3317,27 @@ overwritten (the server does not expose a concurrency token).`
2986
3317
  }
2987
3318
 
2988
3319
  // src/commands/fin/simulations.ts
3320
+ import { readFile as readFile5 } from "fs/promises";
3321
+ import { basename as basename4 } from "path";
3322
+ import { parse as parseYaml4, stringify as stringifyYaml4 } from "yaml";
3323
+ var SIMULATION_SET_TYPE = "simulation_set";
3324
+ function ensureSimulationSetType(raw) {
3325
+ let parsed;
3326
+ try {
3327
+ parsed = parseYaml4(raw);
3328
+ } catch {
3329
+ return raw;
3330
+ }
3331
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
3332
+ return raw;
3333
+ }
3334
+ const obj = parsed;
3335
+ if (obj.type != null && obj.type !== "") {
3336
+ return raw;
3337
+ }
3338
+ obj.type = SIMULATION_SET_TYPE;
3339
+ return stringifyYaml4(obj);
3340
+ }
2989
3341
  function registerSimulationsCommand(fin) {
2990
3342
  const simulations = fin.command("simulations").description("Manage Fin SDK simulations");
2991
3343
  simulations.command("list <procedure_id>").description("List simulations for a procedure (displays YAML)").option("--verbose", "Show HTTP request/response details").action(async function(procedureId) {
@@ -2994,6 +3346,36 @@ function registerSimulationsCommand(fin) {
2994
3346
  const text = await client.getText(`/fin-sdk/procedures/${procedureId}/simulations`);
2995
3347
  console.log(text);
2996
3348
  });
3349
+ simulations.command("upload <file>").description("Upload a simulation set YAML file for its procedure").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
3350
+ "after",
3351
+ `
3352
+ The file is a simulation set carrying its own procedure_id, as produced by
3353
+ \`intercom fin procedures get <id> ./out --with-simulations\` (writes
3354
+ ./out/simulations.yaml) or \`intercom fin simulations list <procedure_id>\`.
3355
+ This completes the export/edit/re-import round-trip for a procedure's tests.
3356
+
3357
+ Example:
3358
+ $ intercom fin simulations upload ./out/simulations.yaml`
3359
+ ).action(async function(filePath) {
3360
+ const opts = this.optsWithGlobals();
3361
+ const client = await resolveClient(opts);
3362
+ const fileContent = await readFile5(filePath, "utf8");
3363
+ const yamlBody = ensureSimulationSetType(fileContent);
3364
+ const formData = new FormData();
3365
+ const blob = new Blob([yamlBody], { type: "application/x-yaml" });
3366
+ formData.append("file_data", blob, basename4(filePath));
3367
+ const result = await client.postFormData("/fin-sdk/upload", formData);
3368
+ render(
3369
+ result,
3370
+ opts,
3371
+ () => detail({
3372
+ "ID": result.id != null ? String(result.id) : void 0,
3373
+ "Procedure ID": result.procedure_id != null ? String(result.procedure_id) : void 0,
3374
+ "Type": result.type != null ? String(result.type) : void 0,
3375
+ "Message": result.message != null ? String(result.message) : void 0
3376
+ })
3377
+ );
3378
+ });
2997
3379
  simulations.command("run <procedure_id> [test_id]").description("Run one or all simulations for a procedure").option("--json", "Output as JSON").option("--verbose", "Show HTTP request/response details").action(async function(procedureId, testId) {
2998
3380
  const opts = this.optsWithGlobals();
2999
3381
  const client = await resolveClient(opts);
@@ -3223,8 +3605,8 @@ Fin is live on workspace ${appId}`);
3223
3605
  }
3224
3606
 
3225
3607
  // src/commands/fin/monitors.ts
3226
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
3227
- import { join as join3, relative, dirname } from "path";
3608
+ import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
3609
+ import { join as join4, relative, dirname } from "path";
3228
3610
  import { stringify } from "yaml";
3229
3611
  async function resolveMonitorId(client, nameOrId) {
3230
3612
  if (/^\d+$/.test(nameOrId)) {
@@ -3305,14 +3687,14 @@ Examples:
3305
3687
  );
3306
3688
  return;
3307
3689
  }
3308
- const outputPath = opts.output ?? join3(
3690
+ const outputPath = opts.output ?? join4(
3309
3691
  process.cwd(),
3310
3692
  "src",
3311
3693
  "qa",
3312
3694
  "bulk-actions",
3313
3695
  "flagged-conversations.yaml"
3314
3696
  );
3315
- await mkdir2(dirname(outputPath), { recursive: true });
3697
+ await mkdir3(dirname(outputPath), { recursive: true });
3316
3698
  await writeFile2(outputPath, buildWorkingSetYaml(result, since), "utf-8");
3317
3699
  const relPath = relative(process.cwd(), outputPath);
3318
3700
  console.log(
@@ -3330,8 +3712,8 @@ Examples:
3330
3712
  }
3331
3713
 
3332
3714
  // src/commands/fin/conversation.ts
3333
- import { readFile as readFile4 } from "fs/promises";
3334
- import { join as join4 } from "path";
3715
+ import { readFile as readFile6 } from "fs/promises";
3716
+ import { join as join5 } from "path";
3335
3717
  import { parse } from "yaml";
3336
3718
  var PLACEHOLDER_MESSAGE = "Your message here \u2014 this will be sent to all conversations listed below.";
3337
3719
  function validateWorkingSet(ws) {
@@ -3381,7 +3763,7 @@ function registerConversationCommand(fin) {
3381
3763
  ).option(
3382
3764
  "--file <path>",
3383
3765
  "Path to the working set YAML file",
3384
- join4(process.cwd(), "src", "qa", "bulk-actions", "flagged-conversations.yaml")
3766
+ join5(process.cwd(), "src", "qa", "bulk-actions", "flagged-conversations.yaml")
3385
3767
  ).option("--dry-run", "Preview actions without sending").option("--yes", "Skip confirmation prompt (required in TTY mode)").option(
3386
3768
  "--json",
3387
3769
  "Output result as JSON"
@@ -3395,7 +3777,7 @@ Examples:
3395
3777
  $ fin conversation bulk-send --json`
3396
3778
  ).action(async function() {
3397
3779
  const opts = this.optsWithGlobals();
3398
- const filePath = opts.file ?? join4(
3780
+ const filePath = opts.file ?? join5(
3399
3781
  process.cwd(),
3400
3782
  "src",
3401
3783
  "qa",
@@ -3404,7 +3786,7 @@ Examples:
3404
3786
  );
3405
3787
  let raw;
3406
3788
  try {
3407
- raw = await readFile4(filePath, "utf-8");
3789
+ raw = await readFile6(filePath, "utf-8");
3408
3790
  } catch (err) {
3409
3791
  const e = err;
3410
3792
  if (e.code === "ENOENT") {
@@ -3565,7 +3947,7 @@ Examples:
3565
3947
 
3566
3948
  // src/commands/setup.ts
3567
3949
  import { readdirSync, readFileSync as readFileSync4, statSync } from "fs";
3568
- import { join as join5, basename as basename3 } from "path";
3950
+ import { join as join6, basename as basename5 } from "path";
3569
3951
  function isGreenfield(opts) {
3570
3952
  return !!(opts.email && opts.name && (process.env.INTERCOM_PASSWORD || opts.passwordStdin));
3571
3953
  }
@@ -3623,8 +4005,8 @@ function buildPlan(opts) {
3623
4005
  }
3624
4006
  if (mdFiles.length > 0) {
3625
4007
  const articleCommands = mdFiles.map((file) => {
3626
- const title = basename3(file, ".md").replace(/-/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase());
3627
- return `intercom articles create -f title="${title}" -F body=@${join5(opts.articlesFrom, file)} -f author_id=$(intercom me --json | jq -r .id)`;
4008
+ const title = basename5(file, ".md").replace(/-/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase());
4009
+ return `intercom articles create -f title="${title}" -F body=@${join6(opts.articlesFrom, file)} -f author_id=$(intercom me --json | jq -r .id)`;
3628
4010
  });
3629
4011
  steps.push({
3630
4012
  step: stepNum++,
@@ -3690,8 +4072,8 @@ async function runPostAuthSteps(client, opts, results, appId) {
3690
4072
  let created = 0;
3691
4073
  let skipped = 0;
3692
4074
  for (const file of mdFiles) {
3693
- const title = basename3(file, ".md").replace(/-/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase());
3694
- const body = readFileSync4(join5(opts.articlesFrom, file), "utf-8");
4075
+ const title = basename5(file, ".md").replace(/-/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase());
4076
+ const body = readFileSync4(join6(opts.articlesFrom, file), "utf-8");
3695
4077
  if (existingTitles.has(title)) {
3696
4078
  skipped++;
3697
4079
  } else {
@@ -4168,7 +4550,7 @@ function registerPricingCommand(program) {
4168
4550
  }
4169
4551
 
4170
4552
  // src/commands/config.ts
4171
- import { join as join6 } from "path";
4553
+ import { join as join7 } from "path";
4172
4554
  var SUPPORTED_KEYS = ["default_workspace", "timeout"];
4173
4555
  function configDir() {
4174
4556
  return process.env.INTERCOM_CONFIG_DIR ?? defaultConfigDir();
@@ -4257,7 +4639,7 @@ function registerConfigCommand(program) {
4257
4639
  console.log(`Set ${key} = ${value}`);
4258
4640
  });
4259
4641
  cmd.command("path").description("Show configuration file path").action(async function() {
4260
- console.log(join6(configDir(), "config.json"));
4642
+ console.log(join7(configDir(), "config.json"));
4261
4643
  });
4262
4644
  }
4263
4645
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intercom/cli",
3
- "version": "0.8.3",
3
+ "version": "0.10.0",
4
4
  "description": "CLI tool for managing Intercom workspaces",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.23.0",
@@ -41,9 +41,5 @@
41
41
  "tsup": "^8.5.1",
42
42
  "typescript": "^5.9.3",
43
43
  "vitest": "^4.1.0"
44
- },
45
- "pnpm": {
46
- "overrides": {},
47
- "onlyBuiltDependencies": []
48
44
  }
49
45
  }