Skip to content

Commit 10fd265

Browse files
committed
feat: support pulling a single resource by UUID
Allow pull commands to target one remote resource by UUID when paired with a single resource type, and clarify the required syntax in the docs.
1 parent 3b19d3c commit 10fd265

4 files changed

Lines changed: 96 additions & 7 deletions

File tree

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ This project manages **Vapi voice agent configurations** as code. All resources
2727
| Test webhook event delivery locally | Run `npm run mock:webhook` and tunnel with ngrok |
2828
| Push changes to Vapi | `npm run push:dev` or `npm run push:prod` |
2929
| Pull latest from Vapi | `npm run pull:dev`, `npm run pull:dev:force`, or `npm run pull:dev:bootstrap` |
30+
| Pull one known remote resource | `npm run pull:dev -- assistants --id <uuid>` |
3031
| Push only one file | `npm run push:dev resources/dev/assistants/my-agent.md` |
3132
| Test a call | `npm run call:dev -- -a <assistant-name>` |
3233

@@ -704,6 +705,8 @@ Concrete example conversations showing expected behavior.
704705
npm run pull:dev # Pull from Vapi (preserve local changes)
705706
npm run pull:dev:force # Pull from Vapi (overwrite everything)
706707
npm run pull:dev:bootstrap # Refresh state without writing remote resources locally
708+
npm run pull:dev -- squads --id <uuid> # Pull one known remote resource by UUID
709+
# `--id` requires exactly one resource type; it will error if omitted or combined with multiple types
707710
npm run push:dev # Push all local changes to Vapi
708711
npm run push:dev assistants # Push only assistants
709712
npm run push:dev resources/dev/assistants/my-agent.md # Push single file

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,34 @@ This mode:
133133

134134
If you skip this step, `push` will automatically run the same bootstrap sync when it detects empty or stale state for the resources being applied.
135135

136+
#### Pulling A Single Resource By UUID
137+
138+
If you know the remote Vapi UUID for a specific resource, you can pull just that resource by combining exactly one resource type with `--id`:
139+
140+
```bash
141+
# Materialize one squad locally
142+
npm run pull:dev -- squads --id <squad-uuid>
143+
144+
# Refresh state only for one assistant
145+
npm run pull:dev:bootstrap -- assistants --id <assistant-uuid>
146+
```
147+
148+
Notes:
149+
150+
- `--id` currently supports remote Vapi UUIDs only
151+
- `--id` must be paired with exactly one resource type such as `assistants`, `squads`, or `tools`
152+
- Single-resource pull updates only the targeted resource mappings and preserves the rest of the state file
153+
154+
This will error if you do not provide exactly one resource type:
155+
156+
```bash
157+
# Invalid: no resource type
158+
npm run pull:dev -- --id <uuid>
159+
160+
# Invalid: more than one resource type
161+
npm run pull:dev -- assistants squads --id <uuid>
162+
```
163+
136164
Promotion example:
137165

138166
```bash

src/config.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { VALID_ENVIRONMENTS, VALID_RESOURCE_TYPES } from "./types.ts";
1111
export interface ApplyFilter {
1212
resourceTypes?: ResourceType[]; // Filter by resource types
1313
filePaths?: string[]; // Apply only specific files
14+
resourceIds?: string[]; // Pull only specific remote resource IDs
1415
}
1516

1617
// Group aliases: expand a shorthand into multiple resource types
@@ -106,6 +107,8 @@ function parseFlags(): {
106107
result.applyFilter.resourceTypes = resolved;
107108
}
108109

110+
const resourceIds: string[] = [];
111+
109112
// Parse file paths and positional resource types
110113
const filePaths: string[] = [];
111114
for (let i = 0; i < args.length; i++) {
@@ -115,10 +118,11 @@ function parseFlags(): {
115118
if (
116119
arg === "--force" ||
117120
arg === "--bootstrap" ||
121+
arg === "--id" ||
118122
arg === "--type" ||
119123
arg === "-t"
120124
) {
121-
if (arg === "--type" || arg === "-t") i++; // skip the value too
125+
if (arg === "--type" || arg === "-t" || arg === "--id") i++; // skip the value too
122126
continue;
123127
}
124128
// Check if it's a resource type or group (positional)
@@ -139,6 +143,18 @@ function parseFlags(): {
139143
result.applyFilter.filePaths = filePaths;
140144
}
141145

146+
for (let i = 0; i < args.length; i++) {
147+
const arg = args[i];
148+
if (arg === "--id" && args[i + 1]) {
149+
resourceIds.push(args[i + 1]!);
150+
i++;
151+
}
152+
}
153+
154+
if (resourceIds.length > 0) {
155+
result.applyFilter.resourceIds = resourceIds;
156+
}
157+
142158
return result;
143159
}
144160

src/pull.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,7 @@ export interface PullOptions {
527527
force?: boolean;
528528
bootstrap?: boolean;
529529
typeFilter?: ResourceType[];
530+
resourceIds?: string[];
530531
}
531532

532533
export interface PullResult {
@@ -543,23 +544,45 @@ export async function pullResourceType(
543544
changedFiles?: Set<string>;
544545
force?: boolean;
545546
bootstrap?: boolean;
547+
resourceIds?: string[];
546548
} = {},
547549
): Promise<PullStats> {
548-
const { changedFiles, force, bootstrap } = options;
550+
const { changedFiles, force, bootstrap, resourceIds } = options;
549551
console.log(`\n📥 Pulling ${resourceType}...`);
550552

551-
const resources = (await fetchAllResources(resourceType)) ?? [];
553+
const allResources = (await fetchAllResources(resourceType)) ?? [];
552554

553-
if (!Array.isArray(resources)) {
555+
if (!Array.isArray(allResources)) {
554556
console.log(` ⚠️ No ${resourceType} found (API returned non-array)`);
555557
return { created: 0, updated: 0, skipped: 0 };
556558
}
557559

558-
console.log(` Found ${resources.length} ${resourceType} in Vapi`);
560+
let resources = allResources;
561+
if (resourceIds?.length) {
562+
const requestedIds = new Set(resourceIds);
563+
resources = allResources.filter((resource) =>
564+
requestedIds.has(resource.id),
565+
);
566+
const foundIds = new Set(resources.map((resource) => resource.id));
567+
const missingIds = resourceIds.filter((id) => !foundIds.has(id));
568+
569+
console.log(
570+
` Found ${resources.length} matching ${resourceType} in Vapi (requested ${resourceIds.length})`,
571+
);
572+
if (missingIds.length > 0) {
573+
console.log(
574+
` ⚠️ Requested IDs not found for ${resourceType}: ${missingIds.join(", ")}`,
575+
);
576+
}
577+
} else {
578+
console.log(` Found ${resources.length} ${resourceType} in Vapi`);
579+
}
559580

560581
const reverseMap = buildReverseMap(state, resourceType);
561582
const credReverse = credentialReverseMap(state);
562-
const newStateSection: Record<string, string> = {};
583+
const newStateSection: Record<string, string> = resourceIds?.length
584+
? { ...state[resourceType] }
585+
: {};
563586

564587
let created = 0;
565588
let updated = 0;
@@ -672,6 +695,15 @@ export async function runPull(options: PullOptions = {}): Promise<PullResult> {
672695
const force = options.force ?? process.argv.includes("--force");
673696
const bootstrap = options.bootstrap ?? BOOTSTRAP_SYNC;
674697
const typeFilter = options.typeFilter ?? APPLY_FILTER.resourceTypes;
698+
const resourceIds = options.resourceIds ?? APPLY_FILTER.resourceIds;
699+
700+
if (resourceIds?.length) {
701+
if (!typeFilter?.length || typeFilter.length !== 1) {
702+
throw new Error(
703+
"Single-resource pull requires exactly one resource type. Example: npm run pull:dev -- squads --id <uuid>",
704+
);
705+
}
706+
}
675707

676708
console.log(
677709
"═══════════════════════════════════════════════════════════════",
@@ -683,6 +715,9 @@ export async function runPull(options: PullOptions = {}): Promise<PullResult> {
683715
if (typeFilter?.length) {
684716
console.log(` Filter: ${typeFilter.join(", ")}`);
685717
}
718+
if (resourceIds?.length) {
719+
console.log(` IDs: ${resourceIds.join(", ")}`);
720+
}
686721
if (bootstrap) {
687722
console.log(
688723
" Mode: state sync only (remote resources are not written locally)",
@@ -749,48 +784,55 @@ export async function runPull(options: PullOptions = {}): Promise<PullResult> {
749784
changedFiles,
750785
force,
751786
bootstrap,
787+
resourceIds,
752788
});
753789
if (shouldPull("assistants"))
754790
stats.assistants = await pullResourceType("assistants", state, {
755791
changedFiles,
756792
force,
757793
bootstrap,
794+
resourceIds,
758795
});
759796
if (shouldPull("structuredOutputs"))
760797
stats.structuredOutputs = await pullResourceType(
761798
"structuredOutputs",
762799
state,
763-
{ changedFiles, force, bootstrap },
800+
{ changedFiles, force, bootstrap, resourceIds },
764801
);
765802
if (shouldPull("squads"))
766803
stats.squads = await pullResourceType("squads", state, {
767804
changedFiles,
768805
force,
769806
bootstrap,
807+
resourceIds,
770808
});
771809
if (shouldPull("personalities"))
772810
stats.personalities = await pullResourceType("personalities", state, {
773811
changedFiles,
774812
force,
775813
bootstrap,
814+
resourceIds,
776815
});
777816
if (shouldPull("scenarios"))
778817
stats.scenarios = await pullResourceType("scenarios", state, {
779818
changedFiles,
780819
force,
781820
bootstrap,
821+
resourceIds,
782822
});
783823
if (shouldPull("simulations"))
784824
stats.simulations = await pullResourceType("simulations", state, {
785825
changedFiles,
786826
force,
787827
bootstrap,
828+
resourceIds,
788829
});
789830
if (shouldPull("simulationSuites"))
790831
stats.simulationSuites = await pullResourceType("simulationSuites", state, {
791832
changedFiles,
792833
force,
793834
bootstrap,
835+
resourceIds,
794836
});
795837

796838
await saveState(state);

0 commit comments

Comments
 (0)