@intercom/cli 0.9.0 → 0.11.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 +19 -0
  2. package/dist/index.js +313 -38
  3. package/package.json +1 -5
package/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ 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.11.0] - 2026-08-17
11
+
12
+ ### Added
13
+
14
+ - `fin workflows versions <id>` lists a workflow's server-side version history, showing which version is live, which is draft, and who saved it. `fin workflows 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.
15
+
16
+ ## [0.10.0] - 2026-08-17
17
+
18
+ ### Added
19
+
20
+ - `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`.
21
+ - `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.
22
+ - `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.
23
+ - `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`).
24
+
25
+ ### Changed
26
+
27
+ - `--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.
28
+
10
29
  ## [0.9.0] - 2026-07-03
11
30
 
12
31
  ### Added
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
  }
@@ -2572,6 +2583,46 @@ import { writeFile as writeFile3 } from "fs/promises";
2572
2583
  import { readFile as readFile2, mkdir as mkdir2 } from "fs/promises";
2573
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,7 +2642,7 @@ function registerProceduresCommand(fin) {
2591
2642
  return table({ columns: ["ID", "NAME"], rows });
2592
2643
  }, items);
2593
2644
  });
2594
- 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("--verbose", "Show HTTP request/response details").addHelpText(
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(
2595
2646
  "after",
2596
2647
  `
2597
2648
  Examples:
@@ -2599,22 +2650,26 @@ Examples:
2599
2650
  $ intercom fin procedures get proc-1 procedure.yaml # write YAML to a file
2600
2651
  $ intercom fin procedures get proc-1 ./out --with-simulations
2601
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
2602
2655
 
2603
2656
  With --with-simulations, [output] is treated as a directory (default: current
2604
2657
  directory). Without it, [output] is a single file for the procedure YAML.
2605
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
+
2606
2664
  Re-import the pieces with \`intercom fin procedures upload <procedure.yaml>\`
2607
2665
  and \`intercom fin simulations upload <simulations.yaml>\`.`
2608
2666
  ).action(async function(id, output) {
2609
2667
  const opts = this.optsWithGlobals();
2610
- const client = await resolveClient(opts);
2611
- const buffer = await client.getBuffer(`/fin-sdk/procedures/${id}/download`);
2612
- const zip = new AdmZip(buffer);
2613
- const entry = zip.getEntries().find((e) => e.entryName.endsWith("procedure.yaml"));
2614
- if (!entry) {
2615
- throw new Error("procedure.yaml not found in downloaded ZIP");
2668
+ if (opts.version && opts.withSimulations) {
2669
+ throw new CLIError("--version cannot be combined with --with-simulations (simulations are not versioned)");
2616
2670
  }
2617
- const procedureYaml = zip.readAsText(entry);
2671
+ const client = await resolveClient(opts);
2672
+ const procedureYaml = opts.version ? await getVersionYaml(client, id, opts.version) : await getCurrentYaml(client, id);
2618
2673
  let simulationsYaml;
2619
2674
  if (opts.withSimulations) {
2620
2675
  simulationsYaml = await client.getText(`/fin-sdk/procedures/${id}/simulations`);
@@ -2637,17 +2692,163 @@ and \`intercom fin simulations upload <simulations.yaml>\`.`
2637
2692
  console.log(procedureYaml);
2638
2693
  }
2639
2694
  });
2640
- 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) {
2641
2712
  const opts = this.optsWithGlobals();
2642
2713
  const client = await resolveClient(opts);
2643
2714
  const fileContent = await readFile2(filePath);
2644
2715
  const formData = new FormData();
2645
2716
  const blob = new Blob([fileContent], { type: "application/x-yaml" });
2646
2717
  formData.append("file_data", blob, basename(filePath));
2718
+ if (opts.note) {
2719
+ formData.append("version_note", opts.note);
2720
+ }
2647
2721
  const result = await client.postFormData("/fin-sdk/upload", formData);
2648
2722
  render(result, opts, () => formatJson(result));
2723
+ if (opts.verify) {
2724
+ verifySaved(filePath, fileContent.toString("utf8"), result);
2725
+ }
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);
2649
2790
  });
2650
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
+ );
2851
+ }
2651
2852
 
2652
2853
  // src/commands/fin/data-connectors.ts
2653
2854
  function summarizeKeys(obj) {
@@ -2803,34 +3004,37 @@ Optional fields: state (live or paused, default: paused), audiences`
2803
3004
  // src/commands/fin/attributes.ts
2804
3005
  import { readFile as readFile3 } from "fs/promises";
2805
3006
  import { basename as basename2 } from "path";
2806
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
3007
+ import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
2807
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
+ }
2808
3013
  function buildAttributesUploadBody(raw) {
2809
3014
  let parsed;
2810
3015
  try {
2811
- parsed = parseYaml(raw);
3016
+ parsed = parseYaml2(raw);
2812
3017
  } catch {
2813
3018
  return raw;
2814
3019
  }
2815
3020
  if (Array.isArray(parsed)) {
2816
- return stringifyYaml({ type: ATTRIBUTE_TYPE, attributes: parsed });
3021
+ return stringifyYaml2({ type: ATTRIBUTE_TYPE, attributes: parsed });
2817
3022
  }
2818
3023
  if (parsed == null || typeof parsed !== "object") {
2819
3024
  return raw;
2820
3025
  }
2821
3026
  const obj = parsed;
2822
3027
  if (Array.isArray(obj.attributes)) {
2823
- return stringifyYaml({ ...obj, type: ATTRIBUTE_TYPE });
3028
+ return stringifyYaml2({ ...obj, type: ATTRIBUTE_TYPE });
2824
3029
  }
2825
- return stringifyYaml({ type: ATTRIBUTE_TYPE, attributes: [obj] });
3030
+ return stringifyYaml2({ type: ATTRIBUTE_TYPE, attributes: [obj] });
2826
3031
  }
2827
3032
  function registerAttributesCommand(fin) {
2828
3033
  const attributes = fin.command("attributes").description("Manage Fin SDK attributes");
2829
3034
  attributes.command("list").description("List Fin SDK attributes").option("--json", "Output as JSON").option("--verbose", "Show HTTP request/response details").action(async function() {
2830
3035
  const opts = this.optsWithGlobals();
2831
3036
  const client = await resolveClient(opts);
2832
- const result = await client.get("/fin-sdk/attributes");
2833
- const items = Array.isArray(result) ? result : result.attributes ?? [];
3037
+ const items = await fetchAttributes(client);
2834
3038
  render(items, opts, () => {
2835
3039
  if (items.length === 0) {
2836
3040
  return "No attributes found.";
@@ -2843,6 +3047,32 @@ function registerAttributesCommand(fin) {
2843
3047
  return table({ columns: ["NAME", "TYPE", "DESCRIPTION"], rows });
2844
3048
  }, items);
2845
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
+ });
2846
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(
2847
3077
  "after",
2848
3078
  `
@@ -2912,14 +3142,14 @@ function registerAudiencesCommand(fin) {
2912
3142
  // src/commands/fin/workflows.ts
2913
3143
  import { readFile as readFile4 } from "fs/promises";
2914
3144
  import { basename as basename3 } from "path";
2915
- import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
2916
- var STATE_LIVE = "live";
2917
- var STATE_PAUSED = "paused";
3145
+ import { parse as parseYaml3, stringify as stringifyYaml3 } from "yaml";
3146
+ var STATE_LIVE2 = "live";
3147
+ var STATE_PAUSED2 = "paused";
2918
3148
  var WORKFLOW_TYPE = "workflow";
2919
3149
  function ensureWorkflowType(raw) {
2920
3150
  let parsed;
2921
3151
  try {
2922
- parsed = parseYaml2(raw);
3152
+ parsed = parseYaml3(raw);
2923
3153
  } catch {
2924
3154
  return raw;
2925
3155
  }
@@ -2931,7 +3161,7 @@ function ensureWorkflowType(raw) {
2931
3161
  return raw;
2932
3162
  }
2933
3163
  obj.type = WORKFLOW_TYPE;
2934
- return stringifyYaml2(obj);
3164
+ return stringifyYaml3(obj);
2935
3165
  }
2936
3166
  function registerWorkflowsCommand(fin) {
2937
3167
  const workflows = fin.command("workflows").description("Manage Fin SDK workflows");
@@ -2953,22 +3183,28 @@ function registerWorkflowsCommand(fin) {
2953
3183
  return table({ columns: ["ID", "TITLE", "STATE", "TRIGGER"], rows });
2954
3184
  }, items);
2955
3185
  });
2956
- workflows.command("get <id> [output]").description("Get a workflow by ID as YAML (writes to [output] file, or stdout)").option("--json", "Output as parsed JSON instead of YAML").option("--jq <expr>", "Filter parsed-JSON output with a jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
3186
+ workflows.command("get <id> [output]").description("Get a workflow by ID as YAML (writes to [output] file, or stdout)").option("--json", "Output as parsed JSON instead of YAML").option("--jq <expr>", "Filter parsed-JSON output with a jq expression").option("--version <version_id>", "Download a historical version instead of the current one").option("--verbose", "Show HTTP request/response details").addHelpText(
2957
3187
  "after",
2958
3188
  `
2959
3189
  Examples:
2960
3190
  $ intercom fin workflows get 42 workflow.yaml # write YAML to a file
2961
3191
  $ intercom fin workflows get 42 > workflow.yaml # or redirect stdout
2962
3192
  $ intercom fin workflows get 42 --json | jq .state
3193
+ $ intercom fin workflows versions 42 # find a version ID
3194
+ $ intercom fin workflows get 42 --version 4821 # print that version's YAML
2963
3195
 
2964
3196
  The YAML it prints is ready to edit and re-upload with
2965
- \`intercom fin workflows upload\`.`
3197
+ \`intercom fin workflows upload\`.
3198
+
3199
+ --version downloads the re-uploadable YAML of a historical version, so you can
3200
+ diff a local file against what is live (\`intercom fin workflows versions <id>\`
3201
+ shows which version that is).`
2966
3202
  ).action(async function(id, output) {
2967
3203
  const opts = this.optsWithGlobals();
2968
3204
  const client = await resolveClient(opts);
2969
- const text = await client.getText(`/fin-sdk/workflows/${id}`);
3205
+ const text = opts.version ? await getVersionYaml2(client, id, opts.version) : await client.getText(`/fin-sdk/workflows/${id}`);
2970
3206
  if (opts.json || opts.jq || opts.agent) {
2971
- const parsed = parseYaml2(text);
3207
+ const parsed = parseYaml3(text);
2972
3208
  render(parsed, opts, () => formatJson(parsed));
2973
3209
  return;
2974
3210
  }
@@ -3026,13 +3262,13 @@ overwritten (the server does not expose a concurrency token).`
3026
3262
  const text = await client.getText(`/fin-sdk/workflows/${id}`);
3027
3263
  let parsed;
3028
3264
  try {
3029
- parsed = parseYaml2(text);
3265
+ parsed = parseYaml3(text);
3030
3266
  } catch (err) {
3031
3267
  throw new Error(`server returned unparseable YAML for workflow ${id}: ${err.message}`);
3032
3268
  }
3033
3269
  parsed.type = WORKFLOW_TYPE;
3034
- parsed.state = STATE_LIVE;
3035
- const yamlBody = stringifyYaml2(parsed);
3270
+ parsed.state = STATE_LIVE2;
3271
+ const yamlBody = stringifyYaml3(parsed);
3036
3272
  const formData = new FormData();
3037
3273
  const blob = new Blob([yamlBody], { type: "application/x-yaml" });
3038
3274
  formData.append("file_data", blob, `workflow-${id}.yaml`);
@@ -3063,13 +3299,13 @@ overwritten (the server does not expose a concurrency token).`
3063
3299
  const text = await client.getText(`/fin-sdk/workflows/${id}`);
3064
3300
  let parsed;
3065
3301
  try {
3066
- parsed = parseYaml2(text);
3302
+ parsed = parseYaml3(text);
3067
3303
  } catch (err) {
3068
3304
  throw new Error(`server returned unparseable YAML for workflow ${id}: ${err.message}`);
3069
3305
  }
3070
3306
  parsed.type = WORKFLOW_TYPE;
3071
- parsed.state = STATE_PAUSED;
3072
- const yamlBody = stringifyYaml2(parsed);
3307
+ parsed.state = STATE_PAUSED2;
3308
+ const yamlBody = stringifyYaml3(parsed);
3073
3309
  const formData = new FormData();
3074
3310
  const blob = new Blob([yamlBody], { type: "application/x-yaml" });
3075
3311
  formData.append("file_data", blob, `workflow-${id}.yaml`);
@@ -3084,17 +3320,56 @@ overwritten (the server does not expose a concurrency token).`
3084
3320
  })
3085
3321
  );
3086
3322
  });
3323
+ workflows.command("versions <id>").description("List a workflow'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(
3324
+ "after",
3325
+ `
3326
+ Examples:
3327
+ $ intercom fin workflows versions 42
3328
+ $ intercom fin workflows versions 42 --jq '.entity_version_id'
3329
+
3330
+ STATUS is the server's own vocabulary: live is what Fin is running now, draft is
3331
+ unpublished, previously_live is superseded. Download any of them with
3332
+ \`intercom fin workflows get <id> --version <VERSION>\`.`
3333
+ ).action(async function(id) {
3334
+ const opts = this.optsWithGlobals();
3335
+ const client = await resolveClient(opts);
3336
+ const result = await client.get(`/fin-sdk/workflows/${id}/versions`);
3337
+ const items = result.versions ?? [];
3338
+ render(items, opts, () => {
3339
+ if (items.length === 0) {
3340
+ return "No versions found.";
3341
+ }
3342
+ const rows = items.map((v) => ({
3343
+ VERSION: String(v.entity_version_id ?? ""),
3344
+ STATUS: String(v.status ?? ""),
3345
+ CREATED: String(v.created_at ?? ""),
3346
+ AUTHOR: String(v.author?.name ?? ""),
3347
+ NOTE: String(v.note ?? "")
3348
+ }));
3349
+ return table({ columns: ["VERSION", "STATUS", "CREATED", "AUTHOR", "NOTE"], rows });
3350
+ }, items);
3351
+ });
3352
+ }
3353
+ async function getVersionYaml2(client, id, versionId) {
3354
+ const result = await client.get(
3355
+ `/fin-sdk/workflows/${id}/versions/${versionId}`
3356
+ );
3357
+ const content = result.content;
3358
+ if (content == null) {
3359
+ throw new CLIError(`Workflow version ${versionId} returned no content`);
3360
+ }
3361
+ return typeof content === "string" ? content : stringifyYaml3(content);
3087
3362
  }
3088
3363
 
3089
3364
  // src/commands/fin/simulations.ts
3090
3365
  import { readFile as readFile5 } from "fs/promises";
3091
3366
  import { basename as basename4 } from "path";
3092
- import { parse as parseYaml3, stringify as stringifyYaml3 } from "yaml";
3367
+ import { parse as parseYaml4, stringify as stringifyYaml4 } from "yaml";
3093
3368
  var SIMULATION_SET_TYPE = "simulation_set";
3094
3369
  function ensureSimulationSetType(raw) {
3095
3370
  let parsed;
3096
3371
  try {
3097
- parsed = parseYaml3(raw);
3372
+ parsed = parseYaml4(raw);
3098
3373
  } catch {
3099
3374
  return raw;
3100
3375
  }
@@ -3106,7 +3381,7 @@ function ensureSimulationSetType(raw) {
3106
3381
  return raw;
3107
3382
  }
3108
3383
  obj.type = SIMULATION_SET_TYPE;
3109
- return stringifyYaml3(obj);
3384
+ return stringifyYaml4(obj);
3110
3385
  }
3111
3386
  function registerSimulationsCommand(fin) {
3112
3387
  const simulations = fin.command("simulations").description("Manage Fin SDK simulations");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intercom/cli",
3
- "version": "0.9.0",
3
+ "version": "0.11.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
  }