@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.
- package/CHANGELOG.md +21 -0
- package/dist/index.js +430 -48
- 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
|
-
|
|
158
|
-
|
|
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
|
|
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
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
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").
|
|
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
|
|
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
|
|
2813
|
-
import { basename as
|
|
2814
|
-
import { parse as
|
|
2815
|
-
var
|
|
2816
|
-
var
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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,
|
|
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 =
|
|
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 =
|
|
2934
|
-
const yamlBody =
|
|
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 =
|
|
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 =
|
|
2971
|
-
const yamlBody =
|
|
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
|
|
3227
|
-
import { join as
|
|
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 ??
|
|
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
|
|
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
|
|
3334
|
-
import { join as
|
|
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
|
-
|
|
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 ??
|
|
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
|
|
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
|
|
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 =
|
|
3627
|
-
return `intercom articles create -f title="${title}" -F body=@${
|
|
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 =
|
|
3694
|
-
const body = readFileSync4(
|
|
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
|
|
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(
|
|
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.
|
|
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
|
}
|