struere 0.8.2 → 0.9.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.
@@ -19780,7 +19780,7 @@ async function browserLoginInternal(spinner) {
19780
19780
  }
19781
19781
  function printNextSteps() {
19782
19782
  console.log(source_default.gray("You can now use:"));
19783
- console.log(source_default.gray(" \u2022"), source_default.cyan("struere dev"), source_default.gray("- Start cloud-connected dev server"));
19783
+ console.log(source_default.gray(" \u2022"), source_default.cyan("struere sync"), source_default.gray("- Sync resources to development"));
19784
19784
  console.log(source_default.gray(" \u2022"), source_default.cyan("struere deploy"), source_default.gray("- Deploy your agent"));
19785
19785
  console.log(source_default.gray(" \u2022"), source_default.cyan("struere status"), source_default.gray("- Compare local vs remote state"));
19786
19786
  console.log();
@@ -20883,6 +20883,33 @@ function buildDocument(projectContext) {
20883
20883
  lines.push("");
20884
20884
  lines.push(`> This is a Struere workspace project. You define agents, entity types, roles, triggers, and custom tools here. The CLI syncs them to the Convex backend.`);
20885
20885
  lines.push("");
20886
+ lines.push(`## Agent Usage`);
20887
+ lines.push("");
20888
+ lines.push("If you are an AI coding agent (Claude Code, Cursor, Copilot, etc.), use these patterns:");
20889
+ lines.push("");
20890
+ lines.push("**Auth**: Set `STRUERE_API_KEY` environment variable (no browser login needed)");
20891
+ lines.push("");
20892
+ lines.push("**Sync**: Use `struere sync` instead of `struere dev` \u2014 it syncs once and exits (no watch loop)");
20893
+ lines.push("```bash");
20894
+ lines.push("struere sync # sync to development + eval, then exit");
20895
+ lines.push("struere sync --json # machine-readable JSON output");
20896
+ lines.push("struere sync --env production # sync to production");
20897
+ lines.push("struere sync --force # skip deletion confirmations");
20898
+ lines.push("```");
20899
+ lines.push("");
20900
+ lines.push("**Deploy**: `struere deploy --force` skips confirmation prompts");
20901
+ lines.push("");
20902
+ lines.push("**JSON output**: Most commands support `--json` for structured output:");
20903
+ lines.push("```bash");
20904
+ lines.push("struere entities list <type> --json");
20905
+ lines.push("struere status --json");
20906
+ lines.push("struere deploy --json --force");
20907
+ lines.push("```");
20908
+ lines.push("");
20909
+ lines.push("**Non-interactive mode** is auto-detected when `STRUERE_API_KEY` is set or stdout is not a TTY. In this mode, all confirmation prompts are auto-accepted and spinners are replaced with plain text.");
20910
+ lines.push("");
20911
+ lines.push("**Exit codes**: All commands exit `0` on success, `1` on error. Check `$?` after execution.");
20912
+ lines.push("");
20886
20913
  if (projectContext) {
20887
20914
  lines.push(projectContext);
20888
20915
  lines.push("");
@@ -20904,6 +20931,7 @@ function buildDocument(projectContext) {
20904
20931
  lines.push("");
20905
20932
  lines.push("| Command | Description |");
20906
20933
  lines.push("|---------|-------------|");
20934
+ lines.push("| `struere sync` | One-shot sync to Convex and exit (agent-friendly) |");
20907
20935
  lines.push("| `struere dev` | Watch files and sync to Convex on save |");
20908
20936
  lines.push("| `struere deploy` | Push development config to production |");
20909
20937
  lines.push("| `struere add agent\\|entity-type\\|role\\|trigger\\|eval\\|fixture <name>` | Scaffold a new resource |");
@@ -21014,7 +21042,7 @@ function buildDocument(projectContext) {
21014
21042
  lines.push("- **Keep tools under 10 per agent.** Agents perform significantly worse when they have too many tools to choose from. If an agent needs more, split it into specialist agents and use `agent.chat` to orchestrate");
21015
21043
  lines.push("- **Always ask the user before making assumptions.** The user may not be technical \u2014 help them accomplish what they want by asking the right questions and offering clear options");
21016
21044
  lines.push("- **Always check the documentation before making changes.** Fetch the relevant doc link below to verify the correct API, field names, and patterns. Do not guess \u2014 wrong field names or patterns will cause silent failures");
21017
- lines.push("- **Use `struere dev` to validate changes.** Every save triggers a sync \u2014 check the terminal for errors before testing");
21045
+ lines.push("- **Use `struere sync` to validate changes.** Run after editing files to sync to Convex. Use `struere dev` for continuous watch mode during manual development");
21018
21046
  lines.push("- **Test with evals.** Write eval suites to catch regressions in agent behavior (`struere add eval <name>`)");
21019
21047
  lines.push("");
21020
21048
  lines.push(`## Documentation`);
@@ -21046,6 +21074,7 @@ function buildDocument(projectContext) {
21046
21074
  lines.push("### CLI");
21047
21075
  lines.push(`- [CLI Overview](${DOCS_BASE}/cli/overview.md)`);
21048
21076
  lines.push(`- [struere init](${DOCS_BASE}/cli/init.md)`);
21077
+ lines.push(`- [struere sync](${DOCS_BASE}/cli/sync.md)`);
21049
21078
  lines.push(`- [struere dev](${DOCS_BASE}/cli/dev.md)`);
21050
21079
  lines.push(`- [struere add](${DOCS_BASE}/cli/add.md)`);
21051
21080
  lines.push(`- [struere deploy](${DOCS_BASE}/cli/deploy.md)`);
@@ -21133,11 +21162,84 @@ var docsCommand = new Command("docs").description("Generate AI context files (CL
21133
21162
  console.log();
21134
21163
  });
21135
21164
 
21165
+ // src/cli/utils/runtime.ts
21166
+ function isInteractive2() {
21167
+ return process.stdout.isTTY === true && !process.env.CI && !getApiKey();
21168
+ }
21169
+ function createOutput() {
21170
+ if (isInteractive2()) {
21171
+ const spinner = ora();
21172
+ return {
21173
+ start(msg) {
21174
+ spinner.start(msg);
21175
+ },
21176
+ succeed(msg) {
21177
+ spinner.succeed(msg);
21178
+ },
21179
+ fail(msg) {
21180
+ spinner.fail(msg);
21181
+ },
21182
+ stop() {
21183
+ spinner.stop();
21184
+ },
21185
+ info(msg) {
21186
+ console.log(source_default.gray(msg));
21187
+ },
21188
+ warn(msg) {
21189
+ console.log(source_default.yellow(msg));
21190
+ },
21191
+ error(msg) {
21192
+ console.error(source_default.red(msg));
21193
+ },
21194
+ json(data) {
21195
+ console.log(JSON.stringify(data, null, 2));
21196
+ }
21197
+ };
21198
+ }
21199
+ return {
21200
+ start(msg) {
21201
+ console.log(msg);
21202
+ },
21203
+ succeed(msg) {
21204
+ console.log(msg);
21205
+ },
21206
+ fail(msg) {
21207
+ console.error(msg);
21208
+ },
21209
+ stop() {},
21210
+ info(msg) {
21211
+ console.log(msg);
21212
+ },
21213
+ warn(msg) {
21214
+ console.log(msg);
21215
+ },
21216
+ error(msg) {
21217
+ console.error(msg);
21218
+ },
21219
+ json(data) {
21220
+ console.log(JSON.stringify(data));
21221
+ }
21222
+ };
21223
+ }
21224
+ function isAuthError(error) {
21225
+ const message = error instanceof Error ? error.message : String(error);
21226
+ return message.includes("Unauthenticated") || message.includes("OIDC") || message.includes("token") || message.includes("expired");
21227
+ }
21228
+ function isOrgAccessError(error) {
21229
+ const message = error instanceof Error ? error.message : String(error);
21230
+ return message.includes("Access denied") || message.includes("not a member") || message.includes("Organization not found");
21231
+ }
21232
+
21136
21233
  // src/cli/commands/init.ts
21137
21234
  async function runInit(cwd, selectedOrg) {
21138
21235
  const spinner = ora();
21236
+ const nonInteractive = !isInteractive2();
21139
21237
  let credentials = loadCredentials();
21140
21238
  if (!credentials) {
21239
+ if (nonInteractive) {
21240
+ console.log(source_default.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
21241
+ return false;
21242
+ }
21141
21243
  console.log(source_default.yellow("Not logged in - authenticating..."));
21142
21244
  console.log();
21143
21245
  credentials = await performLogin();
@@ -21202,18 +21304,24 @@ async function runInit(cwd, selectedOrg) {
21202
21304
  var initCommand = new Command("init").description("Initialize a new Struere organization project").argument("[project-name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").option("--org <slug>", "Organization slug").action(async (projectNameArg, options) => {
21203
21305
  const cwd = process.cwd();
21204
21306
  const spinner = ora();
21307
+ const nonInteractive = !isInteractive2();
21205
21308
  console.log();
21206
21309
  console.log(source_default.bold("Struere CLI"));
21207
21310
  console.log();
21208
21311
  if (hasProject(cwd)) {
21209
21312
  console.log(source_default.yellow("This project is already initialized."));
21210
21313
  console.log();
21211
- console.log(source_default.gray("Run"), source_default.cyan("struere dev"), source_default.gray("to start development"));
21314
+ console.log(source_default.gray("Run"), source_default.cyan("struere sync"), source_default.gray("to sync changes"));
21212
21315
  console.log();
21213
21316
  return;
21214
21317
  }
21215
21318
  let credentials = loadCredentials();
21216
- if (!credentials) {
21319
+ const apiKey = getApiKey();
21320
+ if (!credentials && !apiKey) {
21321
+ if (nonInteractive) {
21322
+ console.log(source_default.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
21323
+ process.exit(1);
21324
+ }
21217
21325
  console.log(source_default.yellow("Not logged in - authenticating..."));
21218
21326
  console.log();
21219
21327
  credentials = await performLogin();
@@ -21223,8 +21331,10 @@ var initCommand = new Command("init").description("Initialize a new Struere orga
21223
21331
  }
21224
21332
  console.log();
21225
21333
  }
21226
- console.log(source_default.green("\u2713"), "Logged in as", source_default.cyan(credentials.user.name || credentials.user.email));
21227
- const { organizations, error } = await listMyOrganizations(credentials.token);
21334
+ if (credentials) {
21335
+ console.log(source_default.green("\u2713"), "Logged in as", source_default.cyan(credentials.user.name || credentials.user.email));
21336
+ }
21337
+ const { organizations, error } = await listMyOrganizations(credentials?.token || "");
21228
21338
  if (error) {
21229
21339
  console.log(source_default.red("Failed to fetch organizations:"), error);
21230
21340
  process.exit(1);
@@ -21244,6 +21354,10 @@ var initCommand = new Command("init").description("Initialize a new Struere orga
21244
21354
  selectedOrg = found;
21245
21355
  } else if (organizations.length === 1) {
21246
21356
  selectedOrg = organizations[0];
21357
+ } else if (nonInteractive) {
21358
+ console.log(source_default.red("Multiple organizations found. Use --org <slug> to specify one."));
21359
+ console.log(source_default.gray("Available:"), organizations.map((o) => o.slug).join(", "));
21360
+ process.exit(1);
21247
21361
  } else {
21248
21362
  selectedOrg = await esm_default4({
21249
21363
  message: "Select organization:",
@@ -21292,7 +21406,7 @@ var initCommand = new Command("init").description("Initialize a new Struere orga
21292
21406
  console.log();
21293
21407
  console.log(source_default.gray("Next steps:"));
21294
21408
  console.log(source_default.gray(" 1."), source_default.cyan("struere add agent my-agent"), source_default.gray("- Create an agent"));
21295
- console.log(source_default.gray(" 2."), source_default.cyan("struere dev"), source_default.gray("- Start development"));
21409
+ console.log(source_default.gray(" 2."), source_default.cyan("struere sync"), source_default.gray("- Sync to development"));
21296
21410
  console.log();
21297
21411
  });
21298
21412
  function slugify(name) {
@@ -21756,17 +21870,298 @@ function extractHandlerCode(handler) {
21756
21870
  return code;
21757
21871
  }
21758
21872
 
21873
+ // src/cli/commands/sync.ts
21874
+ async function performDevSync(cwd, organizationId) {
21875
+ const resources = await loadAllResources(cwd);
21876
+ if (resources.errors.length > 0) {
21877
+ throw new Error(`${resources.errors.length} resource loading error(s):
21878
+ ${resources.errors.join(`
21879
+ `)}`);
21880
+ }
21881
+ const payload = extractSyncPayload(resources);
21882
+ const devResult = await syncOrganization({
21883
+ agents: payload.agents,
21884
+ entityTypes: payload.entityTypes,
21885
+ roles: payload.roles,
21886
+ triggers: payload.triggers,
21887
+ organizationId,
21888
+ environment: "development"
21889
+ });
21890
+ if (!devResult.success) {
21891
+ throw new Error(devResult.error || "Dev sync failed");
21892
+ }
21893
+ const hasEvalContent = payload.evalSuites && payload.evalSuites.length > 0 || payload.fixtures && payload.fixtures.length > 0;
21894
+ if (hasEvalContent) {
21895
+ const evalResult = await syncOrganization({
21896
+ agents: payload.agents,
21897
+ entityTypes: payload.entityTypes,
21898
+ roles: payload.roles,
21899
+ evalSuites: payload.evalSuites,
21900
+ fixtures: payload.fixtures,
21901
+ organizationId,
21902
+ environment: "eval"
21903
+ });
21904
+ if (!evalResult.success) {
21905
+ throw new Error(evalResult.error || "Eval sync failed");
21906
+ }
21907
+ }
21908
+ return devResult;
21909
+ }
21910
+ async function checkForDeletions(resources, organizationId, environment) {
21911
+ const { state: remoteState } = await getSyncState(organizationId, environment);
21912
+ if (!remoteState)
21913
+ return [];
21914
+ const payload = extractSyncPayload(resources);
21915
+ const localSlugs = {
21916
+ agents: new Set(payload.agents.map((a) => a.slug)),
21917
+ entityTypes: new Set(payload.entityTypes.map((et) => et.slug)),
21918
+ roles: new Set(payload.roles.map((r) => r.name)),
21919
+ evalSuites: new Set((payload.evalSuites || []).map((es) => es.slug)),
21920
+ triggers: new Set((payload.triggers || []).map((t) => t.slug))
21921
+ };
21922
+ const deletions = [];
21923
+ const deletedAgents = remoteState.agents.filter((a) => !localSlugs.agents.has(a.slug)).map((a) => a.name);
21924
+ if (deletedAgents.length > 0)
21925
+ deletions.push({ type: "Agents", remote: remoteState.agents.length, local: payload.agents.length, deleted: deletedAgents });
21926
+ const deletedEntityTypes = remoteState.entityTypes.filter((et) => !localSlugs.entityTypes.has(et.slug)).map((et) => et.name);
21927
+ if (deletedEntityTypes.length > 0)
21928
+ deletions.push({ type: "Entity types", remote: remoteState.entityTypes.length, local: payload.entityTypes.length, deleted: deletedEntityTypes });
21929
+ const deletedRoles = remoteState.roles.filter((r) => !localSlugs.roles.has(r.name)).map((r) => r.name);
21930
+ if (deletedRoles.length > 0)
21931
+ deletions.push({ type: "Roles", remote: remoteState.roles.length, local: payload.roles.length, deleted: deletedRoles });
21932
+ const remoteEvalSuites = remoteState.evalSuites || [];
21933
+ const deletedEvalSuites = remoteEvalSuites.filter((es) => !localSlugs.evalSuites.has(es.slug)).map((es) => es.name);
21934
+ if (deletedEvalSuites.length > 0)
21935
+ deletions.push({ type: "Eval suites", remote: remoteEvalSuites.length, local: (payload.evalSuites || []).length, deleted: deletedEvalSuites });
21936
+ const remoteTriggers = remoteState.triggers || [];
21937
+ const deletedTriggers = remoteTriggers.filter((t) => !localSlugs.triggers.has(t.slug)).map((t) => t.name);
21938
+ if (deletedTriggers.length > 0)
21939
+ deletions.push({ type: "Triggers", remote: remoteTriggers.length, local: (payload.triggers || []).length, deleted: deletedTriggers });
21940
+ return deletions;
21941
+ }
21942
+ async function syncToEnvironment(cwd, organizationId, environment) {
21943
+ const resources = await loadAllResources(cwd);
21944
+ if (resources.errors.length > 0) {
21945
+ throw new Error(`${resources.errors.length} resource loading error(s):
21946
+ ${resources.errors.join(`
21947
+ `)}`);
21948
+ }
21949
+ const payload = extractSyncPayload(resources);
21950
+ if (environment === "eval") {
21951
+ const result = await syncOrganization({
21952
+ agents: payload.agents,
21953
+ entityTypes: payload.entityTypes,
21954
+ roles: payload.roles,
21955
+ evalSuites: payload.evalSuites,
21956
+ fixtures: payload.fixtures,
21957
+ organizationId,
21958
+ environment: "eval"
21959
+ });
21960
+ if (!result.success)
21961
+ throw new Error(result.error || "Eval sync failed");
21962
+ return result;
21963
+ }
21964
+ if (environment === "production") {
21965
+ const result = await syncOrganization({
21966
+ ...payload,
21967
+ organizationId,
21968
+ environment: "production"
21969
+ });
21970
+ if (!result.success)
21971
+ throw new Error(result.error || "Production sync failed");
21972
+ return result;
21973
+ }
21974
+ return performDevSync(cwd, organizationId);
21975
+ }
21976
+ var syncCommand = new Command("sync").description("Sync resources to Convex and exit").option("--force", "Skip destructive sync confirmation").option("--json", "Output results as JSON").option("--dry-run", "Show what would be synced without syncing").option("--env <environment>", "Target environment (development|production|eval)").action(async (options) => {
21977
+ const cwd = process.cwd();
21978
+ const jsonMode = !!options.json;
21979
+ const output = createOutput();
21980
+ const interactive = isInteractive2();
21981
+ const shouldForce = options.force || !interactive;
21982
+ const environment = options.env || "development";
21983
+ const syncEval = !options.env || options.env === "development";
21984
+ if (!hasProject(cwd)) {
21985
+ if (jsonMode) {
21986
+ console.log(JSON.stringify({ success: false, error: "No struere.json found" }));
21987
+ } else {
21988
+ output.fail("No struere.json found. Run struere init first.");
21989
+ }
21990
+ process.exit(1);
21991
+ }
21992
+ const project = loadProject(cwd);
21993
+ if (!project) {
21994
+ if (jsonMode) {
21995
+ console.log(JSON.stringify({ success: false, error: "Failed to load struere.json" }));
21996
+ } else {
21997
+ output.fail("Failed to load struere.json");
21998
+ }
21999
+ process.exit(1);
22000
+ }
22001
+ const credentials = loadCredentials();
22002
+ const apiKey = getApiKey();
22003
+ if (!credentials && !apiKey) {
22004
+ if (jsonMode) {
22005
+ console.log(JSON.stringify({ success: false, error: "Not authenticated. Set STRUERE_API_KEY or run struere login." }));
22006
+ } else {
22007
+ output.fail("Not authenticated. Set STRUERE_API_KEY or run struere login.");
22008
+ }
22009
+ process.exit(1);
22010
+ }
22011
+ if (!jsonMode) {
22012
+ output.info(`Organization: ${project.organization.name}`);
22013
+ output.info(`Environment: ${environment}${syncEval && environment === "development" ? " + eval" : ""}`);
22014
+ console.log();
22015
+ }
22016
+ if (!jsonMode)
22017
+ output.start("Loading resources");
22018
+ let resources;
22019
+ try {
22020
+ resources = await loadAllResources(cwd);
22021
+ if (resources.errors.length > 0) {
22022
+ if (jsonMode) {
22023
+ console.log(JSON.stringify({ success: false, error: `${resources.errors.length} resource loading error(s)`, errors: resources.errors }));
22024
+ } else {
22025
+ output.fail("Failed to load resources");
22026
+ for (const err of resources.errors) {
22027
+ output.error(` ${err}`);
22028
+ }
22029
+ }
22030
+ process.exit(1);
22031
+ }
22032
+ if (!jsonMode && !options.dryRun)
22033
+ output.succeed(`Loaded ${resources.agents.length} agents, ${resources.entityTypes.length} entity types, ${resources.roles.length} roles`);
22034
+ } catch (error) {
22035
+ if (jsonMode) {
22036
+ console.log(JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }));
22037
+ } else {
22038
+ output.fail("Failed to load resources");
22039
+ output.error(error instanceof Error ? error.message : String(error));
22040
+ }
22041
+ process.exit(1);
22042
+ }
22043
+ if (options.dryRun) {
22044
+ const payload = extractSyncPayload(resources);
22045
+ if (!jsonMode)
22046
+ output.stop();
22047
+ let deletions = [];
22048
+ try {
22049
+ deletions = await checkForDeletions(resources, project.organization.id, environment);
22050
+ } catch {}
22051
+ if (jsonMode) {
22052
+ console.log(JSON.stringify({
22053
+ dryRun: true,
22054
+ environment,
22055
+ agents: payload.agents.map((a) => a.slug),
22056
+ entityTypes: payload.entityTypes.map((et) => et.slug),
22057
+ roles: payload.roles.map((r) => r.name),
22058
+ triggers: (payload.triggers || []).map((t) => t.slug),
22059
+ deletions: deletions.map((d) => ({ type: d.type, names: d.deleted }))
22060
+ }));
22061
+ } else {
22062
+ console.log(source_default.bold("Dry run \u2014 nothing will be synced"));
22063
+ console.log();
22064
+ console.log(source_default.gray(" Agents:"), payload.agents.map((a) => a.slug).join(", ") || "none");
22065
+ console.log(source_default.gray(" Entity types:"), payload.entityTypes.map((et) => et.slug).join(", ") || "none");
22066
+ console.log(source_default.gray(" Roles:"), payload.roles.map((r) => r.name).join(", ") || "none");
22067
+ console.log(source_default.gray(" Triggers:"), (payload.triggers || []).map((t) => t.slug).join(", ") || "none");
22068
+ if (deletions.length > 0) {
22069
+ console.log();
22070
+ console.log(source_default.yellow.bold(" Would delete:"));
22071
+ for (const d of deletions) {
22072
+ for (const name of d.deleted) {
22073
+ console.log(source_default.red(` - ${d.type}: ${name}`));
22074
+ }
22075
+ }
22076
+ }
22077
+ console.log();
22078
+ }
22079
+ return;
22080
+ }
22081
+ if (!shouldForce) {
22082
+ if (!jsonMode)
22083
+ output.start("Checking remote state");
22084
+ try {
22085
+ const deletions = await checkForDeletions(resources, project.organization.id, environment);
22086
+ if (!jsonMode)
22087
+ output.stop();
22088
+ if (deletions.length > 0) {
22089
+ console.log(source_default.yellow.bold(" Warning: this sync will DELETE remote resources:"));
22090
+ console.log();
22091
+ for (const d of deletions) {
22092
+ console.log(source_default.yellow(` ${d.type}:`.padEnd(20)), `${d.remote} remote \u2192 ${d.local} local`, source_default.red(`(${d.deleted.length} will be deleted)`));
22093
+ for (const name of d.deleted) {
22094
+ console.log(source_default.red(` - ${name}`));
22095
+ }
22096
+ }
22097
+ console.log();
22098
+ console.log(source_default.gray(" Run"), source_default.cyan("struere pull"), source_default.gray("first to download remote resources."));
22099
+ console.log();
22100
+ const shouldContinue = await esm_default2({ message: "Continue anyway?", default: false });
22101
+ if (!shouldContinue) {
22102
+ console.log(source_default.gray("Aborted."));
22103
+ process.exit(0);
22104
+ }
22105
+ console.log();
22106
+ }
22107
+ } catch {
22108
+ if (!jsonMode)
22109
+ output.stop();
22110
+ }
22111
+ }
22112
+ if (!jsonMode)
22113
+ output.start("Syncing to Convex");
22114
+ try {
22115
+ const result = await syncToEnvironment(cwd, project.organization.id, environment);
22116
+ if (!jsonMode)
22117
+ output.succeed(`Synced to ${environment}`);
22118
+ if (jsonMode) {
22119
+ console.log(JSON.stringify({
22120
+ success: true,
22121
+ environment,
22122
+ agents: {
22123
+ created: result.agents?.created || [],
22124
+ updated: result.agents?.updated || [],
22125
+ deleted: result.agents?.deleted || []
22126
+ },
22127
+ entityTypes: {
22128
+ created: result.entityTypes?.created || [],
22129
+ updated: result.entityTypes?.updated || [],
22130
+ deleted: result.entityTypes?.deleted || []
22131
+ },
22132
+ roles: {
22133
+ created: result.roles?.created || [],
22134
+ updated: result.roles?.updated || [],
22135
+ deleted: result.roles?.deleted || []
22136
+ }
22137
+ }));
22138
+ }
22139
+ } catch (error) {
22140
+ if (jsonMode) {
22141
+ console.log(JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }));
22142
+ } else {
22143
+ output.fail("Sync failed");
22144
+ output.error(error instanceof Error ? error.message : String(error));
22145
+ }
22146
+ process.exit(1);
22147
+ }
22148
+ });
22149
+
21759
22150
  // src/cli/commands/dev.ts
21760
- var devCommand = new Command("dev").description("Sync all resources to development environment").option("--force", "Skip destructive sync confirmation").action(async (options) => {
22151
+ var devCommand = new Command("dev").description("Watch files and sync to development on change (long-running)").option("--force", "Skip destructive sync confirmation").action(async (options) => {
21761
22152
  const spinner = ora();
21762
22153
  const cwd = process.cwd();
21763
22154
  const apiKey = getApiKey();
21764
- const isHeadless = !!apiKey && !loadCredentials()?.token;
22155
+ const nonInteractive = !isInteractive2();
22156
+ if (nonInteractive) {
22157
+ console.error("Error: struere dev is a long-running watch process. Use struere sync instead.");
22158
+ process.exit(1);
22159
+ }
21765
22160
  console.log();
21766
22161
  console.log(source_default.bold("Struere Dev"));
21767
22162
  console.log();
21768
22163
  if (!hasProject(cwd)) {
21769
- if (isHeadless) {
22164
+ if (nonInteractive) {
21770
22165
  console.log(source_default.red("No struere.json found. Cannot run init in headless mode."));
21771
22166
  process.exit(1);
21772
22167
  }
@@ -21783,8 +22178,8 @@ var devCommand = new Command("dev").description("Sync all resources to developme
21783
22178
  generateTypeDeclarations(cwd);
21784
22179
  console.log(source_default.gray("Organization:"), source_default.cyan(project.organization.name));
21785
22180
  console.log(source_default.gray("Environment:"), source_default.cyan("development"), "+", source_default.cyan("eval"));
21786
- if (isHeadless) {
21787
- console.log(source_default.gray("Auth:"), source_default.cyan("API key (headless)"));
22181
+ if (nonInteractive) {
22182
+ console.log(source_default.gray("Auth:"), source_default.cyan("non-interactive"));
21788
22183
  }
21789
22184
  console.log();
21790
22185
  let credentials = loadCredentials();
@@ -21809,53 +22204,6 @@ var devCommand = new Command("dev").description("Sync all resources to developme
21809
22204
  console.log(source_default.yellow("\u26A0"), "Could not fetch docs for CLAUDE.md");
21810
22205
  }
21811
22206
  }
21812
- const isAuthError = (error) => {
21813
- const message = error instanceof Error ? error.message : String(error);
21814
- return message.includes("Unauthenticated") || message.includes("OIDC") || message.includes("token") || message.includes("expired");
21815
- };
21816
- const isOrgAccessError = (error) => {
21817
- const message = error instanceof Error ? error.message : String(error);
21818
- return message.includes("Access denied") || message.includes("not a member") || message.includes("Organization not found");
21819
- };
21820
- const performSync = async () => {
21821
- const resources = await loadAllResources(cwd);
21822
- if (resources.errors.length > 0) {
21823
- for (const err of resources.errors) {
21824
- console.log(source_default.red(" \u2716"), err);
21825
- }
21826
- throw new Error(`${resources.errors.length} resource loading error(s):
21827
- ${resources.errors.join(`
21828
- `)}`);
21829
- }
21830
- const payload = extractSyncPayload(resources);
21831
- const devResult = await syncOrganization({
21832
- agents: payload.agents,
21833
- entityTypes: payload.entityTypes,
21834
- roles: payload.roles,
21835
- triggers: payload.triggers,
21836
- organizationId: project.organization.id,
21837
- environment: "development"
21838
- });
21839
- if (!devResult.success) {
21840
- throw new Error(devResult.error || "Dev sync failed");
21841
- }
21842
- const hasEvalContent = payload.evalSuites && payload.evalSuites.length > 0 || payload.fixtures && payload.fixtures.length > 0;
21843
- if (hasEvalContent) {
21844
- const evalResult = await syncOrganization({
21845
- agents: payload.agents,
21846
- entityTypes: payload.entityTypes,
21847
- roles: payload.roles,
21848
- evalSuites: payload.evalSuites,
21849
- fixtures: payload.fixtures,
21850
- organizationId: project.organization.id,
21851
- environment: "eval"
21852
- });
21853
- if (!evalResult.success) {
21854
- throw new Error(evalResult.error || "Eval sync failed");
21855
- }
21856
- }
21857
- return true;
21858
- };
21859
22207
  let initialSyncOk = false;
21860
22208
  let loadedResources = null;
21861
22209
  spinner.start("Loading resources");
@@ -21872,59 +22220,31 @@ ${resources.errors.join(`
21872
22220
  spinner.fail("Failed to load resources");
21873
22221
  console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
21874
22222
  }
21875
- const shouldSkipConfirmation = options.force || isHeadless;
22223
+ const shouldSkipConfirmation = options.force || nonInteractive;
21876
22224
  if (initialSyncOk && !shouldSkipConfirmation && loadedResources) {
21877
22225
  spinner.start("Checking remote state");
21878
22226
  try {
21879
- const { state: remoteState } = await getSyncState(project.organization.id, "development");
22227
+ const deletions = await checkForDeletions(loadedResources, project.organization.id, "development");
21880
22228
  spinner.stop();
21881
- if (remoteState) {
21882
- const payload = extractSyncPayload(loadedResources);
21883
- const localSlugs = {
21884
- agents: new Set(payload.agents.map((a) => a.slug)),
21885
- entityTypes: new Set(payload.entityTypes.map((et) => et.slug)),
21886
- roles: new Set(payload.roles.map((r) => r.name)),
21887
- evalSuites: new Set((payload.evalSuites || []).map((es) => es.slug)),
21888
- triggers: new Set((payload.triggers || []).map((t) => t.slug))
21889
- };
21890
- const deletions = [];
21891
- const deletedAgents = remoteState.agents.filter((a) => !localSlugs.agents.has(a.slug)).map((a) => a.name);
21892
- if (deletedAgents.length > 0)
21893
- deletions.push({ type: "Agents", remote: remoteState.agents.length, local: payload.agents.length, deleted: deletedAgents });
21894
- const deletedEntityTypes = remoteState.entityTypes.filter((et) => !localSlugs.entityTypes.has(et.slug)).map((et) => et.name);
21895
- if (deletedEntityTypes.length > 0)
21896
- deletions.push({ type: "Entity types", remote: remoteState.entityTypes.length, local: payload.entityTypes.length, deleted: deletedEntityTypes });
21897
- const deletedRoles = remoteState.roles.filter((r) => !localSlugs.roles.has(r.name)).map((r) => r.name);
21898
- if (deletedRoles.length > 0)
21899
- deletions.push({ type: "Roles", remote: remoteState.roles.length, local: payload.roles.length, deleted: deletedRoles });
21900
- const remoteEvalSuites = remoteState.evalSuites || [];
21901
- const deletedEvalSuites = remoteEvalSuites.filter((es) => !localSlugs.evalSuites.has(es.slug)).map((es) => es.name);
21902
- if (deletedEvalSuites.length > 0)
21903
- deletions.push({ type: "Eval suites", remote: remoteEvalSuites.length, local: (payload.evalSuites || []).length, deleted: deletedEvalSuites });
21904
- const remoteTriggers = remoteState.triggers || [];
21905
- const deletedTriggers = remoteTriggers.filter((t) => !localSlugs.triggers.has(t.slug)).map((t) => t.name);
21906
- if (deletedTriggers.length > 0)
21907
- deletions.push({ type: "Triggers", remote: remoteTriggers.length, local: (payload.triggers || []).length, deleted: deletedTriggers });
21908
- if (deletions.length > 0) {
21909
- console.log(source_default.yellow.bold(" Warning: this sync will DELETE remote resources:"));
21910
- console.log();
21911
- for (const d of deletions) {
21912
- console.log(source_default.yellow(` ${d.type}:`.padEnd(20)), `${d.remote} remote \u2192 ${d.local} local`, source_default.red(`(${d.deleted.length} will be deleted)`));
21913
- for (const name of d.deleted) {
21914
- console.log(source_default.red(` - ${name}`));
21915
- }
21916
- }
21917
- console.log();
21918
- console.log(source_default.gray(" Run"), source_default.cyan("struere pull"), source_default.gray("first to download remote resources."));
21919
- console.log();
21920
- const shouldContinue = await esm_default2({ message: "Continue anyway?", default: false });
21921
- if (!shouldContinue) {
21922
- console.log();
21923
- console.log(source_default.gray("Aborted."));
21924
- process.exit(0);
22229
+ if (deletions.length > 0) {
22230
+ console.log(source_default.yellow.bold(" Warning: this sync will DELETE remote resources:"));
22231
+ console.log();
22232
+ for (const d of deletions) {
22233
+ console.log(source_default.yellow(` ${d.type}:`.padEnd(20)), `${d.remote} remote \u2192 ${d.local} local`, source_default.red(`(${d.deleted.length} will be deleted)`));
22234
+ for (const name of d.deleted) {
22235
+ console.log(source_default.red(` - ${name}`));
21925
22236
  }
22237
+ }
22238
+ console.log();
22239
+ console.log(source_default.gray(" Run"), source_default.cyan("struere pull"), source_default.gray("first to download remote resources."));
22240
+ console.log();
22241
+ const shouldContinue = await esm_default2({ message: "Continue anyway?", default: false });
22242
+ if (!shouldContinue) {
21926
22243
  console.log();
22244
+ console.log(source_default.gray("Aborted."));
22245
+ process.exit(0);
21927
22246
  }
22247
+ console.log();
21928
22248
  }
21929
22249
  } catch {
21930
22250
  spinner.stop();
@@ -21933,10 +22253,10 @@ ${resources.errors.join(`
21933
22253
  if (initialSyncOk) {
21934
22254
  spinner.start("Syncing to Convex");
21935
22255
  try {
21936
- await performSync();
22256
+ await performDevSync(cwd, project.organization.id);
21937
22257
  spinner.succeed("Synced to development");
21938
22258
  } catch (error) {
21939
- if (isAuthError(error) && !isHeadless) {
22259
+ if (isAuthError(error) && !nonInteractive) {
21940
22260
  spinner.fail("Session expired - re-authenticating...");
21941
22261
  clearCredentials();
21942
22262
  credentials = await performLogin();
@@ -21946,13 +22266,13 @@ ${resources.errors.join(`
21946
22266
  }
21947
22267
  spinner.start("Syncing to Convex");
21948
22268
  try {
21949
- await performSync();
22269
+ await performDevSync(cwd, project.organization.id);
21950
22270
  spinner.succeed("Synced to development");
21951
22271
  } catch (retryError) {
21952
22272
  spinner.fail("Sync failed");
21953
22273
  console.log(source_default.red("Error:"), retryError instanceof Error ? retryError.message : String(retryError));
21954
22274
  }
21955
- } else if (isAuthError(error) && isHeadless) {
22275
+ } else if (isAuthError(error) && nonInteractive) {
21956
22276
  spinner.fail("API key authentication failed");
21957
22277
  console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
21958
22278
  console.log(source_default.gray("Check that STRUERE_API_KEY is valid and not expired."));
@@ -21992,15 +22312,15 @@ ${resources.errors.join(`
21992
22312
  persistent: true,
21993
22313
  usePolling: false
21994
22314
  });
21995
- watcher.on("change", async (path) => {
22315
+ const handleFileChange = async (path, action) => {
21996
22316
  const relativePath = path.replace(cwd, ".");
21997
- console.log(source_default.gray(`Changed: ${relativePath}`));
22317
+ console.log(source_default.gray(`${action}: ${relativePath}`));
21998
22318
  const syncSpinner = ora("Syncing...").start();
21999
22319
  try {
22000
- await performSync();
22320
+ await performDevSync(cwd, project.organization.id);
22001
22321
  syncSpinner.succeed("Synced");
22002
22322
  } catch (error) {
22003
- if (isAuthError(error) && !isHeadless) {
22323
+ if (isAuthError(error) && !nonInteractive) {
22004
22324
  syncSpinner.fail("Session expired - re-authenticating...");
22005
22325
  clearCredentials();
22006
22326
  const newCredentials = await performLogin();
@@ -22010,7 +22330,7 @@ ${resources.errors.join(`
22010
22330
  }
22011
22331
  const retrySyncSpinner = ora("Syncing...").start();
22012
22332
  try {
22013
- await performSync();
22333
+ await performDevSync(cwd, project.organization.id);
22014
22334
  retrySyncSpinner.succeed("Synced");
22015
22335
  } catch (retryError) {
22016
22336
  retrySyncSpinner.fail("Sync failed");
@@ -22021,31 +22341,10 @@ ${resources.errors.join(`
22021
22341
  console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
22022
22342
  }
22023
22343
  }
22024
- });
22025
- watcher.on("add", async (path) => {
22026
- const relativePath = path.replace(cwd, ".");
22027
- console.log(source_default.gray(`Added: ${relativePath}`));
22028
- const syncSpinner = ora("Syncing...").start();
22029
- try {
22030
- await performSync();
22031
- syncSpinner.succeed("Synced");
22032
- } catch (error) {
22033
- syncSpinner.fail("Sync failed");
22034
- console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
22035
- }
22036
- });
22037
- watcher.on("unlink", async (path) => {
22038
- const relativePath = path.replace(cwd, ".");
22039
- console.log(source_default.gray(`Removed: ${relativePath}`));
22040
- const syncSpinner = ora("Syncing...").start();
22041
- try {
22042
- await performSync();
22043
- syncSpinner.succeed("Synced");
22044
- } catch (error) {
22045
- syncSpinner.fail("Sync failed");
22046
- console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
22047
- }
22048
- });
22344
+ };
22345
+ watcher.on("change", (path) => handleFileChange(path, "Changed"));
22346
+ watcher.on("add", (path) => handleFileChange(path, "Added"));
22347
+ watcher.on("unlink", (path) => handleFileChange(path, "Removed"));
22049
22348
  process.on("SIGINT", () => {
22050
22349
  console.log();
22051
22350
  console.log(source_default.gray("Stopping..."));
@@ -22055,21 +22354,26 @@ ${resources.errors.join(`
22055
22354
  });
22056
22355
 
22057
22356
  // src/cli/commands/deploy.ts
22058
- var isAuthError = (error) => {
22059
- const message = error instanceof Error ? error.message : String(error);
22060
- return message.includes("Unauthenticated") || message.includes("OIDC") || message.includes("token") || message.includes("expired");
22061
- };
22062
- var isOrgAccessError = (error) => {
22063
- const message = error instanceof Error ? error.message : String(error);
22064
- return message.includes("Access denied") || message.includes("not a member") || message.includes("Organization not found");
22065
- };
22066
- var deployCommand = new Command("deploy").description("Deploy all resources to production").option("--dry-run", "Show what would be deployed without deploying").action(async (options) => {
22357
+ var deployCommand = new Command("deploy").description("Deploy all resources to production").option("--dry-run", "Show what would be deployed without deploying").option("--force", "Skip destructive sync confirmation").option("--json", "Output results as JSON").action(async (options) => {
22067
22358
  const spinner = ora();
22068
22359
  const cwd = process.cwd();
22069
- console.log();
22070
- console.log(source_default.bold("Deploying to Production"));
22071
- console.log();
22360
+ const nonInteractive = !isInteractive2();
22361
+ const shouldForce = options.force || nonInteractive;
22362
+ const jsonMode = !!options.json;
22363
+ if (!jsonMode) {
22364
+ console.log();
22365
+ console.log(source_default.bold("Deploying to Production"));
22366
+ console.log();
22367
+ }
22072
22368
  if (!hasProject(cwd)) {
22369
+ if (nonInteractive) {
22370
+ if (jsonMode) {
22371
+ console.log(JSON.stringify({ success: false, error: "No struere.json found" }));
22372
+ } else {
22373
+ console.log(source_default.red("No struere.json found. Run struere init first."));
22374
+ }
22375
+ process.exit(1);
22376
+ }
22073
22377
  console.log(source_default.yellow("No struere.json found - initializing project..."));
22074
22378
  console.log();
22075
22379
  const success = await runInit(cwd);
@@ -22080,15 +22384,29 @@ var deployCommand = new Command("deploy").description("Deploy all resources to p
22080
22384
  }
22081
22385
  const project = loadProject(cwd);
22082
22386
  if (!project) {
22083
- console.log(source_default.red("Failed to load struere.json"));
22387
+ if (jsonMode) {
22388
+ console.log(JSON.stringify({ success: false, error: "Failed to load struere.json" }));
22389
+ } else {
22390
+ console.log(source_default.red("Failed to load struere.json"));
22391
+ }
22084
22392
  process.exit(1);
22085
22393
  }
22086
- console.log(source_default.gray("Organization:"), source_default.cyan(project.organization.name));
22087
- console.log(source_default.gray("Environment:"), source_default.cyan("production"));
22088
- console.log();
22394
+ if (!jsonMode) {
22395
+ console.log(source_default.gray("Organization:"), source_default.cyan(project.organization.name));
22396
+ console.log(source_default.gray("Environment:"), source_default.cyan("production"));
22397
+ console.log();
22398
+ }
22089
22399
  let credentials = loadCredentials();
22090
22400
  const apiKey = getApiKey();
22091
22401
  if (!credentials && !apiKey) {
22402
+ if (nonInteractive) {
22403
+ if (jsonMode) {
22404
+ console.log(JSON.stringify({ success: false, error: "Not authenticated. Set STRUERE_API_KEY or run struere login." }));
22405
+ } else {
22406
+ console.log(source_default.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
22407
+ }
22408
+ process.exit(1);
22409
+ }
22092
22410
  console.log(source_default.yellow("Not logged in - authenticating..."));
22093
22411
  console.log();
22094
22412
  credentials = await performLogin();
@@ -22098,66 +22416,68 @@ var deployCommand = new Command("deploy").description("Deploy all resources to p
22098
22416
  }
22099
22417
  console.log();
22100
22418
  }
22101
- spinner.start("Loading resources");
22419
+ if (!jsonMode)
22420
+ spinner.start("Loading resources");
22102
22421
  let resources;
22103
22422
  try {
22104
22423
  resources = await loadAllResources(cwd);
22105
- spinner.succeed(`Loaded ${resources.agents.length} agents, ${resources.entityTypes.length} entity types, ${resources.roles.length} roles, ${resources.customTools.length} custom tools, ${resources.evalSuites.length} eval suites`);
22424
+ if (!jsonMode)
22425
+ spinner.succeed(`Loaded ${resources.agents.length} agents, ${resources.entityTypes.length} entity types, ${resources.roles.length} roles, ${resources.customTools.length} custom tools, ${resources.evalSuites.length} eval suites`);
22106
22426
  for (const err of resources.errors) {
22107
- console.log(source_default.red(" \u2716"), err);
22427
+ if (!jsonMode)
22428
+ console.log(source_default.red(" \u2716"), err);
22108
22429
  }
22109
22430
  if (resources.errors.length > 0) {
22431
+ if (jsonMode) {
22432
+ console.log(JSON.stringify({ success: false, error: `${resources.errors.length} resource loading error(s)`, errors: resources.errors }));
22433
+ }
22110
22434
  process.exit(1);
22111
22435
  }
22112
22436
  } catch (error) {
22113
- spinner.fail("Failed to load resources");
22114
- console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
22437
+ if (jsonMode) {
22438
+ console.log(JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }));
22439
+ } else {
22440
+ spinner.fail("Failed to load resources");
22441
+ console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
22442
+ }
22115
22443
  process.exit(1);
22116
22444
  }
22117
22445
  if (resources.agents.length === 0) {
22118
- console.log();
22119
- console.log(source_default.yellow("No agents found to deploy"));
22120
- console.log();
22121
- console.log(source_default.gray("Run"), source_default.cyan("struere add agent my-agent"), source_default.gray("to create an agent"));
22122
- console.log();
22446
+ if (jsonMode) {
22447
+ console.log(JSON.stringify({ success: false, error: "No agents found to deploy" }));
22448
+ } else {
22449
+ console.log();
22450
+ console.log(source_default.yellow("No agents found to deploy"));
22451
+ console.log();
22452
+ console.log(source_default.gray("Run"), source_default.cyan("struere add agent my-agent"), source_default.gray("to create an agent"));
22453
+ console.log();
22454
+ }
22123
22455
  return;
22124
22456
  }
22125
22457
  const payload = extractSyncPayload(resources);
22126
- spinner.start("Checking remote state");
22458
+ if (!jsonMode)
22459
+ spinner.start("Checking remote state");
22127
22460
  let deletions = [];
22128
22461
  try {
22129
- const { state: remoteState } = await getSyncState(project.organization.id, "production");
22130
- spinner.stop();
22131
- if (remoteState) {
22132
- const localSlugs = {
22133
- agents: new Set(payload.agents.map((a) => a.slug)),
22134
- entityTypes: new Set(payload.entityTypes.map((et) => et.slug)),
22135
- roles: new Set(payload.roles.map((r) => r.name)),
22136
- evalSuites: new Set((payload.evalSuites || []).map((es) => es.slug)),
22137
- triggers: new Set((payload.triggers || []).map((t) => t.slug))
22138
- };
22139
- const deletedAgents = remoteState.agents.filter((a) => !localSlugs.agents.has(a.slug)).map((a) => a.name);
22140
- if (deletedAgents.length > 0)
22141
- deletions.push({ type: "Agents", remote: remoteState.agents.length, local: payload.agents.length, deleted: deletedAgents });
22142
- const deletedEntityTypes = remoteState.entityTypes.filter((et) => !localSlugs.entityTypes.has(et.slug)).map((et) => et.name);
22143
- if (deletedEntityTypes.length > 0)
22144
- deletions.push({ type: "Entity types", remote: remoteState.entityTypes.length, local: payload.entityTypes.length, deleted: deletedEntityTypes });
22145
- const deletedRoles = remoteState.roles.filter((r) => !localSlugs.roles.has(r.name)).map((r) => r.name);
22146
- if (deletedRoles.length > 0)
22147
- deletions.push({ type: "Roles", remote: remoteState.roles.length, local: payload.roles.length, deleted: deletedRoles });
22148
- const remoteEvalSuites = remoteState.evalSuites || [];
22149
- const deletedEvalSuites = remoteEvalSuites.filter((es) => !localSlugs.evalSuites.has(es.slug)).map((es) => es.name);
22150
- if (deletedEvalSuites.length > 0)
22151
- deletions.push({ type: "Eval suites", remote: remoteEvalSuites.length, local: (payload.evalSuites || []).length, deleted: deletedEvalSuites });
22152
- const remoteTriggers = remoteState.triggers || [];
22153
- const deletedTriggers = remoteTriggers.filter((t) => !localSlugs.triggers.has(t.slug)).map((t) => t.name);
22154
- if (deletedTriggers.length > 0)
22155
- deletions.push({ type: "Triggers", remote: remoteTriggers.length, local: (payload.triggers || []).length, deleted: deletedTriggers });
22156
- }
22462
+ deletions = await checkForDeletions(resources, project.organization.id, "production");
22463
+ if (!jsonMode)
22464
+ spinner.stop();
22157
22465
  } catch {
22158
- spinner.stop();
22466
+ if (!jsonMode)
22467
+ spinner.stop();
22159
22468
  }
22160
22469
  if (options.dryRun) {
22470
+ if (jsonMode) {
22471
+ console.log(JSON.stringify({
22472
+ success: true,
22473
+ dryRun: true,
22474
+ agents: resources.agents.map((a) => ({ name: a.name, slug: a.slug, version: a.version })),
22475
+ entityTypes: resources.entityTypes.map((et) => ({ name: et.name, slug: et.slug })),
22476
+ roles: resources.roles.map((r) => ({ name: r.name })),
22477
+ deletions: deletions.flatMap((d) => d.deleted.map((name) => ({ type: d.type.toLowerCase(), name })))
22478
+ }));
22479
+ return;
22480
+ }
22161
22481
  console.log();
22162
22482
  console.log(source_default.yellow("Dry run mode - no changes will be made"));
22163
22483
  console.log();
@@ -22194,7 +22514,7 @@ var deployCommand = new Command("deploy").description("Deploy all resources to p
22194
22514
  console.log();
22195
22515
  return;
22196
22516
  }
22197
- if (deletions.length > 0) {
22517
+ if (deletions.length > 0 && !shouldForce) {
22198
22518
  console.log(source_default.yellow.bold(" Warning: this deploy will DELETE production resources:"));
22199
22519
  console.log();
22200
22520
  for (const d of deletions) {
@@ -22212,7 +22532,8 @@ var deployCommand = new Command("deploy").description("Deploy all resources to p
22212
22532
  }
22213
22533
  console.log();
22214
22534
  }
22215
- spinner.start("Deploying to production");
22535
+ if (!jsonMode)
22536
+ spinner.start("Deploying to production");
22216
22537
  try {
22217
22538
  const syncResult = await syncOrganization({
22218
22539
  ...payload,
@@ -22222,38 +22543,67 @@ var deployCommand = new Command("deploy").description("Deploy all resources to p
22222
22543
  if (!syncResult.success) {
22223
22544
  throw new Error(syncResult.error || "Deploy failed");
22224
22545
  }
22225
- spinner.succeed("Deployed to production");
22226
- console.log();
22227
- console.log(source_default.green("Success!"), "All resources deployed to production");
22228
- console.log();
22229
- if (syncResult.agents?.created && syncResult.agents.created.length > 0) {
22230
- console.log("New agents:");
22231
- for (const slug of syncResult.agents.created) {
22232
- const agent = resources.agents.find((a) => a.slug === slug);
22233
- console.log(source_default.gray(" -"), source_default.cyan(agent?.name || slug));
22546
+ if (!jsonMode)
22547
+ spinner.succeed("Deployed to production");
22548
+ if (jsonMode) {
22549
+ console.log(JSON.stringify({
22550
+ success: true,
22551
+ environment: "production",
22552
+ agents: {
22553
+ created: syncResult.agents?.created || [],
22554
+ updated: syncResult.agents?.updated || [],
22555
+ deleted: syncResult.agents?.deleted || []
22556
+ },
22557
+ entityTypes: {
22558
+ created: syncResult.entityTypes?.created || [],
22559
+ updated: syncResult.entityTypes?.updated || [],
22560
+ deleted: syncResult.entityTypes?.deleted || []
22561
+ },
22562
+ roles: {
22563
+ created: syncResult.roles?.created || [],
22564
+ updated: syncResult.roles?.updated || [],
22565
+ deleted: syncResult.roles?.deleted || []
22566
+ }
22567
+ }));
22568
+ } else {
22569
+ console.log();
22570
+ console.log(source_default.green("Success!"), "All resources deployed to production");
22571
+ console.log();
22572
+ if (syncResult.agents?.created && syncResult.agents.created.length > 0) {
22573
+ console.log("New agents:");
22574
+ for (const slug of syncResult.agents.created) {
22575
+ const agent = resources.agents.find((a) => a.slug === slug);
22576
+ console.log(source_default.gray(" -"), source_default.cyan(agent?.name || slug));
22577
+ }
22234
22578
  }
22235
- }
22236
- if (syncResult.agents?.updated && syncResult.agents.updated.length > 0) {
22237
- console.log("Updated agents:");
22238
- for (const slug of syncResult.agents.updated) {
22239
- const agent = resources.agents.find((a) => a.slug === slug);
22240
- console.log(source_default.gray(" -"), source_default.cyan(agent?.name || slug));
22579
+ if (syncResult.agents?.updated && syncResult.agents.updated.length > 0) {
22580
+ console.log("Updated agents:");
22581
+ for (const slug of syncResult.agents.updated) {
22582
+ const agent = resources.agents.find((a) => a.slug === slug);
22583
+ console.log(source_default.gray(" -"), source_default.cyan(agent?.name || slug));
22584
+ }
22241
22585
  }
22586
+ console.log();
22587
+ console.log(source_default.gray("Test your agents:"));
22588
+ console.log(source_default.gray(" $"), source_default.cyan(`curl -X POST https://<agent-slug>.struere.dev/chat -H "Authorization: Bearer YOUR_API_KEY" -d '{"message": "Hello"}'`));
22589
+ console.log();
22242
22590
  }
22243
- console.log();
22244
- console.log(source_default.gray("Test your agents:"));
22245
- console.log(source_default.gray(" $"), source_default.cyan(`curl -X POST https://<agent-slug>.struere.dev/chat -H "Authorization: Bearer YOUR_API_KEY" -d '{"message": "Hello"}'`));
22246
- console.log();
22247
22591
  } catch (error) {
22248
- if (isAuthError(error)) {
22249
- spinner.fail("Session expired - re-authenticating...");
22592
+ if (isAuthError(error) && !nonInteractive) {
22593
+ if (!jsonMode)
22594
+ spinner.fail("Session expired - re-authenticating...");
22250
22595
  clearCredentials();
22251
22596
  credentials = await performLogin();
22252
22597
  if (!credentials) {
22253
- console.log(source_default.red("Authentication failed"));
22598
+ if (jsonMode) {
22599
+ console.log(JSON.stringify({ success: false, error: "Authentication failed" }));
22600
+ } else {
22601
+ console.log(source_default.red("Authentication failed"));
22602
+ }
22254
22603
  process.exit(1);
22255
22604
  }
22256
- spinner.start("Deploying to production");
22605
+ if (!jsonMode)
22606
+ spinner.start("Deploying to production");
22257
22607
  try {
22258
22608
  const syncResult = await syncOrganization({
22259
22609
  ...payload,
@@ -22263,30 +22613,46 @@ var deployCommand = new Command("deploy").description("Deploy all resources to p
22263
22613
  if (!syncResult.success) {
22264
22614
  throw new Error(syncResult.error || "Deploy failed");
22265
22615
  }
22266
- spinner.succeed("Deployed to production");
22267
- console.log();
22268
- console.log(source_default.green("Success!"), "All resources deployed to production");
22269
- console.log();
22616
+ if (!jsonMode) {
22617
+ spinner.succeed("Deployed to production");
22618
+ console.log();
22619
+ console.log(source_default.green("Success!"), "All resources deployed to production");
22620
+ console.log();
22621
+ } else {
22622
+ console.log(JSON.stringify({ success: true, environment: "production" }));
22623
+ }
22270
22624
  } catch (retryError) {
22271
- spinner.fail("Deployment failed");
22272
- console.log(source_default.red("Error:"), retryError instanceof Error ? retryError.message : String(retryError));
22625
+ if (jsonMode) {
22626
+ console.log(JSON.stringify({ success: false, error: retryError instanceof Error ? retryError.message : String(retryError) }));
22627
+ } else {
22628
+ spinner.fail("Deployment failed");
22629
+ console.log(source_default.red("Error:"), retryError instanceof Error ? retryError.message : String(retryError));
22630
+ }
22273
22631
  process.exit(1);
22274
22632
  }
22275
22633
  } else if (isOrgAccessError(error)) {
22276
- spinner.fail("Organization access denied");
22277
- console.log();
22278
- console.log(source_default.red("You do not have access to organization:"), source_default.cyan(project.organization.name));
22279
- console.log();
22280
- console.log(source_default.gray("To fix this:"));
22281
- console.log(source_default.gray(" 1."), "Check that you have access to this organization");
22282
- console.log(source_default.gray(" 2."), "Or run", source_default.cyan("struere init"), "to select a different organization");
22283
- console.log();
22634
+ if (jsonMode) {
22635
+ console.log(JSON.stringify({ success: false, error: `Organization access denied: ${project.organization.name}` }));
22636
+ } else {
22637
+ spinner.fail("Organization access denied");
22638
+ console.log();
22639
+ console.log(source_default.red("You do not have access to organization:"), source_default.cyan(project.organization.name));
22640
+ console.log();
22641
+ console.log(source_default.gray("To fix this:"));
22642
+ console.log(source_default.gray(" 1."), "Check that you have access to this organization");
22643
+ console.log(source_default.gray(" 2."), "Or run", source_default.cyan("struere init"), "to select a different organization");
22644
+ console.log();
22645
+ }
22284
22646
  process.exit(1);
22285
22647
  } else {
22286
- spinner.fail("Deployment failed");
22287
- console.log();
22288
- console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
22289
- console.log();
22648
+ if (jsonMode) {
22649
+ console.log(JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }));
22650
+ } else {
22651
+ spinner.fail("Deployment failed");
22652
+ console.log();
22653
+ console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
22654
+ console.log();
22655
+ }
22290
22656
  process.exit(1);
22291
22657
  }
22292
22658
  }
@@ -22308,32 +22674,47 @@ var logoutCommand = new Command("logout").description("Log out of Struere").acti
22308
22674
  });
22309
22675
 
22310
22676
  // src/cli/commands/whoami.ts
22311
- var whoamiCommand = new Command("whoami").description("Show current logged in user").option("--refresh", "Refresh user info from server").action(async (options) => {
22312
- console.log();
22677
+ var whoamiCommand = new Command("whoami").description("Show current logged in user").option("--refresh", "Refresh user info from server").option("--json", "Output raw JSON").action(async (options) => {
22678
+ const jsonMode = !!options.json;
22313
22679
  const credentials = loadCredentials();
22314
- if (!credentials) {
22315
- console.log(source_default.yellow("Not logged in"));
22316
- console.log();
22317
- console.log(source_default.gray("Run"), source_default.cyan("struere login"), source_default.gray("to log in"));
22318
- console.log();
22680
+ const apiKey = getApiKey();
22681
+ if (!credentials && !apiKey) {
22682
+ if (jsonMode) {
22683
+ console.log(JSON.stringify({ authenticated: false }));
22684
+ } else {
22685
+ console.log();
22686
+ console.log(source_default.yellow("Not logged in"));
22687
+ console.log();
22688
+ console.log(source_default.gray("Run"), source_default.cyan("struere login"), source_default.gray("to log in"));
22689
+ console.log();
22690
+ }
22319
22691
  return;
22320
22692
  }
22321
- if (options.refresh) {
22322
- const spinner = ora("Fetching user info").start();
22693
+ if (options.refresh && credentials) {
22694
+ const spinner = jsonMode ? null : ora("Fetching user info").start();
22323
22695
  const { userInfo, error } = await getUserInfo(credentials.token);
22324
22696
  if (error || !userInfo) {
22325
- spinner.fail("Failed to fetch user info");
22326
- console.log();
22327
- if (error?.includes("401") || error?.includes("unauthorized")) {
22328
- console.log(source_default.red("Session expired. Please log in again."));
22697
+ if (jsonMode) {
22698
+ console.log(JSON.stringify({ authenticated: false, error: error || "Unknown error" }));
22329
22699
  } else {
22330
- console.log(source_default.red("Error:"), error || "Unknown error");
22700
+ spinner?.fail("Failed to fetch user info");
22701
+ console.log();
22702
+ if (error?.includes("401") || error?.includes("unauthorized")) {
22703
+ console.log(source_default.red("Session expired. Please log in again."));
22704
+ } else {
22705
+ console.log(source_default.red("Error:"), error || "Unknown error");
22706
+ }
22707
+ console.log();
22331
22708
  }
22332
- console.log();
22333
22709
  process.exit(1);
22334
22710
  }
22335
- spinner.stop();
22711
+ spinner?.stop();
22336
22712
  const { user, organizations } = userInfo;
22713
+ if (jsonMode) {
22714
+ console.log(JSON.stringify({ user, organizations }));
22715
+ return;
22716
+ }
22717
+ console.log();
22337
22718
  console.log(source_default.bold("Logged in as:"));
22338
22719
  console.log();
22339
22720
  console.log(source_default.gray(" User: "), source_default.cyan(user.name || user.email), source_default.gray(`<${user.email}>`));
@@ -22349,13 +22730,36 @@ var whoamiCommand = new Command("whoami").description("Show current logged in us
22349
22730
  }
22350
22731
  console.log();
22351
22732
  } else {
22352
- console.log(source_default.bold("Logged in as:"));
22353
- console.log();
22354
- console.log(source_default.gray(" User: "), source_default.cyan(credentials.user.name), source_default.gray(`<${credentials.user.email}>`));
22355
- console.log(source_default.gray(" User ID: "), source_default.gray(credentials.user.id));
22356
- console.log();
22357
- console.log(source_default.gray("Use"), source_default.cyan("struere whoami --refresh"), source_default.gray("to fetch organizations"));
22358
- console.log();
22733
+ if (credentials) {
22734
+ if (jsonMode) {
22735
+ console.log(JSON.stringify({
22736
+ user: {
22737
+ id: credentials.user.id,
22738
+ name: credentials.user.name,
22739
+ email: credentials.user.email
22740
+ }
22741
+ }));
22742
+ return;
22743
+ }
22744
+ console.log();
22745
+ console.log(source_default.bold("Logged in as:"));
22746
+ console.log();
22747
+ console.log(source_default.gray(" User: "), source_default.cyan(credentials.user.name), source_default.gray(`<${credentials.user.email}>`));
22748
+ console.log(source_default.gray(" User ID: "), source_default.gray(credentials.user.id));
22749
+ console.log();
22750
+ console.log(source_default.gray("Use"), source_default.cyan("struere whoami --refresh"), source_default.gray("to fetch organizations"));
22751
+ console.log();
22752
+ } else {
22753
+ if (jsonMode) {
22754
+ console.log(JSON.stringify({ authenticated: true, authMethod: "api-key" }));
22755
+ } else {
22756
+ console.log();
22757
+ console.log(source_default.bold("Authenticated via API key"));
22758
+ console.log();
22759
+ console.log(source_default.gray("Use"), source_default.cyan("struere whoami --refresh"), source_default.gray("with browser login for full details"));
22760
+ console.log();
22761
+ }
22762
+ }
22359
22763
  }
22360
22764
  });
22361
22765
 
@@ -22420,7 +22824,7 @@ var addCommand = new Command("add").description("Scaffold a new resource").argum
22420
22824
  console.log(source_default.gray(" \u2192"), file);
22421
22825
  }
22422
22826
  console.log();
22423
- console.log(source_default.gray("Edit the YAML file, then run"), source_default.cyan("struere dev"), source_default.gray("to sync"));
22827
+ console.log(source_default.gray("Edit the YAML file, then run"), source_default.cyan("struere sync"), source_default.gray("to sync"));
22424
22828
  } else {
22425
22829
  console.log(source_default.yellow("Eval suite already exists:"), `evals/${slug}.eval.yaml`);
22426
22830
  }
@@ -22444,7 +22848,7 @@ var addCommand = new Command("add").description("Scaffold a new resource").argum
22444
22848
  console.log(source_default.gray(" \u2192"), file);
22445
22849
  }
22446
22850
  console.log();
22447
- console.log(source_default.gray("Edit the YAML file, then run"), source_default.cyan("struere dev"), source_default.gray("to sync"));
22851
+ console.log(source_default.gray("Edit the YAML file, then run"), source_default.cyan("struere sync"), source_default.gray("to sync"));
22448
22852
  } else {
22449
22853
  console.log(source_default.yellow("Fixture already exists:"), `fixtures/${slug}.fixture.yaml`);
22450
22854
  }
@@ -22463,7 +22867,7 @@ var addCommand = new Command("add").description("Scaffold a new resource").argum
22463
22867
  process.exit(1);
22464
22868
  }
22465
22869
  console.log();
22466
- console.log(source_default.gray("Run"), source_default.cyan("struere dev"), source_default.gray("to sync changes"));
22870
+ console.log(source_default.gray("Run"), source_default.cyan("struere sync"), source_default.gray("to sync changes"));
22467
22871
  console.log();
22468
22872
  });
22469
22873
  function slugify2(name) {
@@ -22471,13 +22875,25 @@ function slugify2(name) {
22471
22875
  }
22472
22876
 
22473
22877
  // src/cli/commands/status.ts
22474
- var statusCommand = new Command("status").description("Compare local vs remote state").action(async () => {
22878
+ var statusCommand = new Command("status").description("Compare local vs remote state").option("--json", "Output raw JSON").action(async (opts) => {
22475
22879
  const spinner = ora();
22476
22880
  const cwd = process.cwd();
22477
- console.log();
22478
- console.log(source_default.bold("Struere Status"));
22479
- console.log();
22881
+ const jsonMode = !!opts.json;
22882
+ const nonInteractive = !isInteractive2();
22883
+ if (!jsonMode) {
22884
+ console.log();
22885
+ console.log(source_default.bold("Struere Status"));
22886
+ console.log();
22887
+ }
22480
22888
  if (!hasProject(cwd)) {
22889
+ if (nonInteractive) {
22890
+ if (jsonMode) {
22891
+ console.log(JSON.stringify({ error: "No struere.json found. Run struere init first." }));
22892
+ } else {
22893
+ console.log(source_default.red("No struere.json found. Run struere init first."));
22894
+ }
22895
+ process.exit(1);
22896
+ }
22481
22897
  console.log(source_default.yellow("No struere.json found - initializing project..."));
22482
22898
  console.log();
22483
22899
  const success = await runInit(cwd);
@@ -22488,14 +22904,28 @@ var statusCommand = new Command("status").description("Compare local vs remote s
22488
22904
  }
22489
22905
  const project = loadProject(cwd);
22490
22906
  if (!project) {
22491
- console.log(source_default.red("Failed to load struere.json"));
22907
+ if (jsonMode) {
22908
+ console.log(JSON.stringify({ error: "Failed to load struere.json" }));
22909
+ } else {
22910
+ console.log(source_default.red("Failed to load struere.json"));
22911
+ }
22492
22912
  process.exit(1);
22493
22913
  }
22494
- console.log(source_default.gray("Organization:"), source_default.cyan(project.organization.name));
22495
- console.log();
22914
+ if (!jsonMode) {
22915
+ console.log(source_default.gray("Organization:"), source_default.cyan(project.organization.name));
22916
+ console.log();
22917
+ }
22496
22918
  let credentials = loadCredentials();
22497
22919
  const apiKey = getApiKey();
22498
22920
  if (!credentials && !apiKey) {
22921
+ if (nonInteractive) {
22922
+ if (jsonMode) {
22923
+ console.log(JSON.stringify({ error: "Not authenticated. Set STRUERE_API_KEY or run struere login." }));
22924
+ } else {
22925
+ console.log(source_default.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
22926
+ }
22927
+ process.exit(1);
22928
+ }
22499
22929
  console.log(source_default.yellow("Not logged in - authenticating..."));
22500
22930
  console.log();
22501
22931
  credentials = await performLogin();
@@ -22505,34 +22935,49 @@ var statusCommand = new Command("status").description("Compare local vs remote s
22505
22935
  }
22506
22936
  console.log();
22507
22937
  }
22508
- spinner.start("Loading local resources");
22938
+ if (!jsonMode)
22939
+ spinner.start("Loading local resources");
22509
22940
  let localResources;
22510
22941
  try {
22511
22942
  localResources = await loadAllResources(cwd);
22512
- spinner.succeed(`Loaded ${localResources.agents.length} agents, ${localResources.entityTypes.length} entity types, ${localResources.roles.length} roles, ${localResources.customTools.length} custom tools, ${localResources.evalSuites.length} eval suites`);
22513
- for (const err of localResources.errors) {
22514
- console.log(source_default.red(" \u2716"), err);
22943
+ if (!jsonMode) {
22944
+ spinner.succeed(`Loaded ${localResources.agents.length} agents, ${localResources.entityTypes.length} entity types, ${localResources.roles.length} roles, ${localResources.customTools.length} custom tools, ${localResources.evalSuites.length} eval suites`);
22945
+ for (const err of localResources.errors) {
22946
+ console.log(source_default.red(" \u2716"), err);
22947
+ }
22515
22948
  }
22516
22949
  if (localResources.errors.length > 0) {
22950
+ if (jsonMode) {
22951
+ console.log(JSON.stringify({ error: `${localResources.errors.length} resource loading error(s)`, errors: localResources.errors }));
22952
+ }
22517
22953
  process.exit(1);
22518
22954
  }
22519
22955
  } catch (error) {
22520
- spinner.fail("Failed to load local resources");
22521
- console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
22956
+ if (jsonMode) {
22957
+ console.log(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
22958
+ } else {
22959
+ spinner.fail("Failed to load local resources");
22960
+ console.log(source_default.red("Error:"), error instanceof Error ? error.message : String(error));
22961
+ }
22522
22962
  process.exit(1);
22523
22963
  }
22524
- spinner.start("Fetching remote state");
22964
+ if (!jsonMode)
22965
+ spinner.start("Fetching remote state");
22525
22966
  const [devResult, prodResult] = await Promise.all([
22526
22967
  getSyncState(undefined, "development"),
22527
22968
  getSyncState(undefined, "production")
22528
22969
  ]);
22529
22970
  if (devResult.error || !devResult.state) {
22530
- spinner.fail("Failed to fetch remote state");
22531
- console.log(source_default.red("Error:"), devResult.error || "Unknown error");
22971
+ if (jsonMode) {
22972
+ console.log(JSON.stringify({ error: devResult.error || "Failed to fetch remote state" }));
22973
+ } else {
22974
+ spinner.fail("Failed to fetch remote state");
22975
+ console.log(source_default.red("Error:"), devResult.error || "Unknown error");
22976
+ }
22532
22977
  process.exit(1);
22533
22978
  }
22534
- spinner.succeed("Remote state fetched");
22535
- console.log();
22979
+ if (!jsonMode)
22980
+ spinner.succeed("Remote state fetched");
22536
22981
  const devState = devResult.state;
22537
22982
  const prodState = prodResult.state;
22538
22983
  const localAgentSlugs = new Set(localResources.agents.map((a) => a.slug));
@@ -22541,6 +22986,24 @@ var statusCommand = new Command("status").description("Compare local vs remote s
22541
22986
  const devEntityTypeSlugs = new Set(devState.entityTypes.map((et) => et.slug));
22542
22987
  const localRoleNames = new Set(localResources.roles.map((r) => r.name));
22543
22988
  const devRoleNames = new Set(devState.roles.map((r) => r.name));
22989
+ if (opts.json) {
22990
+ const classify = (localItems, remoteItems, useSlug) => {
22991
+ const localKeys = new Set(localItems.map((i) => useSlug ? i.slug : i.name));
22992
+ const remoteKeys = new Set(remoteItems.map((i) => useSlug ? i.slug : i.name));
22993
+ return {
22994
+ synced: localItems.filter((i) => remoteKeys.has(useSlug ? i.slug : i.name)).map((i) => useSlug ? i.slug : i.name),
22995
+ new: localItems.filter((i) => !remoteKeys.has(useSlug ? i.slug : i.name)).map((i) => useSlug ? i.slug : i.name),
22996
+ deleted: remoteItems.filter((i) => !localKeys.has(useSlug ? i.slug : i.name)).map((i) => useSlug ? i.slug : i.name)
22997
+ };
22998
+ };
22999
+ console.log(JSON.stringify({
23000
+ agents: classify(localResources.agents, devState.agents, true),
23001
+ entityTypes: classify(localResources.entityTypes, devState.entityTypes, true),
23002
+ roles: classify(localResources.roles, devState.roles, false)
23003
+ }));
23004
+ return;
23005
+ }
23006
+ console.log();
22544
23007
  console.log(source_default.bold("Agents"));
22545
23008
  console.log(source_default.gray("\u2500".repeat(60)));
22546
23009
  if (localResources.agents.length === 0 && devState.agents.length === 0) {
@@ -22609,7 +23072,7 @@ var statusCommand = new Command("status").description("Compare local vs remote s
22609
23072
  console.log(source_default.gray("Legend:"));
22610
23073
  console.log(source_default.gray(" "), source_default.green("\u25CF"), "Synced", source_default.yellow("\u25CB"), "Not in production", source_default.blue("+"), "New", source_default.red("-"), "Will be deleted");
22611
23074
  console.log();
22612
- console.log(source_default.gray("Run"), source_default.cyan("struere dev"), source_default.gray("to sync to development"));
23075
+ console.log(source_default.gray("Run"), source_default.cyan("struere sync"), source_default.gray("to sync to development"));
22613
23076
  console.log(source_default.gray("Run"), source_default.cyan("struere deploy"), source_default.gray("to deploy to production"));
22614
23077
  console.log();
22615
23078
  });
@@ -22908,13 +23371,25 @@ function collectCustomTools(agents) {
22908
23371
  }
22909
23372
 
22910
23373
  // src/cli/commands/pull.ts
22911
- var pullCommand = new Command("pull").description("Pull remote resources to local files").option("--force", "Overwrite existing local files").option("--env <environment>", "Environment to pull from", "development").option("--dry-run", "Show what would be written without writing").action(async (options) => {
23374
+ var pullCommand = new Command("pull").description("Pull remote resources to local files").option("--force", "Overwrite existing local files").option("--env <environment>", "Environment to pull from", "development").option("--dry-run", "Show what would be written without writing").option("--json", "Output raw JSON").action(async (options) => {
22912
23375
  const spinner = ora();
22913
23376
  const cwd = process.cwd();
22914
- console.log();
22915
- console.log(source_default.bold("Struere Pull"));
22916
- console.log();
23377
+ const jsonMode = !!options.json;
23378
+ const nonInteractive = !isInteractive2();
23379
+ if (!jsonMode) {
23380
+ console.log();
23381
+ console.log(source_default.bold("Struere Pull"));
23382
+ console.log();
23383
+ }
22917
23384
  if (!hasProject(cwd)) {
23385
+ if (nonInteractive) {
23386
+ if (jsonMode) {
23387
+ console.log(JSON.stringify({ error: "No struere.json found. Run struere init first." }));
23388
+ } else {
23389
+ console.log(source_default.red("No struere.json found. Run struere init first."));
23390
+ }
23391
+ process.exit(1);
23392
+ }
22918
23393
  console.log(source_default.yellow("No struere.json found - initializing project..."));
22919
23394
  console.log();
22920
23395
  const success = await runInit(cwd);
@@ -22925,15 +23400,29 @@ var pullCommand = new Command("pull").description("Pull remote resources to loca
22925
23400
  }
22926
23401
  const project = loadProject(cwd);
22927
23402
  if (!project) {
22928
- console.log(source_default.red("Failed to load struere.json"));
23403
+ if (jsonMode) {
23404
+ console.log(JSON.stringify({ error: "Failed to load struere.json" }));
23405
+ } else {
23406
+ console.log(source_default.red("Failed to load struere.json"));
23407
+ }
22929
23408
  process.exit(1);
22930
23409
  }
22931
- console.log(source_default.gray("Organization:"), source_default.cyan(project.organization.name));
22932
- console.log(source_default.gray("Environment:"), source_default.cyan(options.env));
22933
- console.log();
23410
+ if (!jsonMode) {
23411
+ console.log(source_default.gray("Organization:"), source_default.cyan(project.organization.name));
23412
+ console.log(source_default.gray("Environment:"), source_default.cyan(options.env));
23413
+ console.log();
23414
+ }
22934
23415
  let credentials = loadCredentials();
22935
23416
  const apiKey = getApiKey();
22936
23417
  if (!credentials && !apiKey) {
23418
+ if (nonInteractive) {
23419
+ if (jsonMode) {
23420
+ console.log(JSON.stringify({ error: "Not authenticated. Set STRUERE_API_KEY or run struere login." }));
23421
+ } else {
23422
+ console.log(source_default.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
23423
+ }
23424
+ process.exit(1);
23425
+ }
22937
23426
  console.log(source_default.yellow("Not logged in - authenticating..."));
22938
23427
  console.log();
22939
23428
  credentials = await performLogin();
@@ -22943,16 +23432,23 @@ var pullCommand = new Command("pull").description("Pull remote resources to loca
22943
23432
  }
22944
23433
  console.log();
22945
23434
  }
22946
- spinner.start("Fetching remote state");
23435
+ if (!jsonMode)
23436
+ spinner.start("Fetching remote state");
22947
23437
  const environment = options.env;
22948
23438
  const { state, error } = await getPullState(project.organization.id, environment);
22949
23439
  if (error || !state) {
22950
- spinner.fail("Failed to fetch remote state");
22951
- console.log(source_default.red("Error:"), error || "Unknown error");
23440
+ if (jsonMode) {
23441
+ console.log(JSON.stringify({ error: error || "Failed to fetch remote state" }));
23442
+ } else {
23443
+ spinner.fail("Failed to fetch remote state");
23444
+ console.log(source_default.red("Error:"), error || "Unknown error");
23445
+ }
22952
23446
  process.exit(1);
22953
23447
  }
22954
- spinner.succeed("Remote state fetched");
22955
- console.log();
23448
+ if (!jsonMode) {
23449
+ spinner.succeed("Remote state fetched");
23450
+ console.log();
23451
+ }
22956
23452
  const created = [];
22957
23453
  const skipped = [];
22958
23454
  const ensureDir2 = (dir) => {
@@ -23031,6 +23527,14 @@ var pullCommand = new Command("pull").description("Pull remote resources to loca
23031
23527
  if (content)
23032
23528
  writeOrSkip("triggers/index.ts", content);
23033
23529
  }
23530
+ if (options.json) {
23531
+ console.log(JSON.stringify({
23532
+ created,
23533
+ skipped,
23534
+ dryRun: !!options.dryRun
23535
+ }));
23536
+ return;
23537
+ }
23034
23538
  if (options.dryRun) {
23035
23539
  console.log(source_default.cyan("Dry run - no files written"));
23036
23540
  console.log();
@@ -23054,7 +23558,7 @@ var pullCommand = new Command("pull").description("Pull remote resources to loca
23054
23558
  console.log();
23055
23559
  }
23056
23560
  if (!options.dryRun && created.length > 0) {
23057
- console.log(source_default.gray("Run"), source_default.cyan("struere dev"), source_default.gray("to sync changes"));
23561
+ console.log(source_default.gray("Run"), source_default.cyan("struere sync"), source_default.gray("to sync changes"));
23058
23562
  console.log();
23059
23563
  }
23060
23564
  });
@@ -23229,7 +23733,12 @@ function deriveColumnsFromSchema(schema, displayConfig) {
23229
23733
  // src/cli/commands/entities.ts
23230
23734
  async function ensureAuth() {
23231
23735
  const cwd = process.cwd();
23736
+ const nonInteractive = !isInteractive2();
23232
23737
  if (!hasProject(cwd)) {
23738
+ if (nonInteractive) {
23739
+ console.error(source_default.red("No struere.json found. Run struere init first."));
23740
+ process.exit(1);
23741
+ }
23233
23742
  console.log(source_default.yellow("No struere.json found - initializing project..."));
23234
23743
  console.log();
23235
23744
  const success = await runInit(cwd);
@@ -23241,6 +23750,10 @@ async function ensureAuth() {
23241
23750
  let credentials = loadCredentials();
23242
23751
  const apiKey = getApiKey();
23243
23752
  if (!credentials && !apiKey) {
23753
+ if (nonInteractive) {
23754
+ console.error(source_default.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
23755
+ process.exit(1);
23756
+ }
23244
23757
  console.log(source_default.yellow("Not logged in - authenticating..."));
23245
23758
  console.log();
23246
23759
  credentials = await performLogin();
@@ -23377,6 +23890,9 @@ entitiesCommand.command("create <type>").description("Create a new entity").opti
23377
23890
  console.log(source_default.red("Invalid JSON in --data"));
23378
23891
  process.exit(1);
23379
23892
  }
23893
+ } else if (!isInteractive2()) {
23894
+ console.log(source_default.red("--data <json> is required in non-interactive mode"));
23895
+ process.exit(1);
23380
23896
  } else {
23381
23897
  spinner.start(`Fetching ${type} schema`);
23382
23898
  const { data: typeData, error: error2 } = await queryEntityTypeBySlug(type, env2);
@@ -23468,32 +23984,41 @@ entitiesCommand.command("update <id>").description("Update an entity").option("-
23468
23984
  console.log();
23469
23985
  }
23470
23986
  });
23471
- entitiesCommand.command("delete <id>").description("Delete an entity").option("--env <environment>", "Environment (development|production)", "development").option("--yes", "Skip confirmation").action(async (id, opts) => {
23987
+ entitiesCommand.command("delete <id>").description("Delete an entity").option("--env <environment>", "Environment (development|production)", "development").option("--yes", "Skip confirmation").option("--json", "Output raw JSON").action(async (id, opts) => {
23472
23988
  await ensureAuth();
23473
23989
  const spinner = ora();
23474
23990
  const env2 = opts.env;
23475
- spinner.start("Fetching entity");
23991
+ const jsonMode = !!opts.json;
23992
+ if (!jsonMode)
23993
+ spinner.start("Fetching entity");
23476
23994
  const { data, error: fetchError } = await queryEntity(id, env2);
23477
23995
  if (fetchError || !data) {
23478
- spinner.fail("Failed to fetch entity");
23479
- console.log(source_default.red("Error:"), fetchError || "Entity not found");
23996
+ if (jsonMode) {
23997
+ console.log(JSON.stringify({ success: false, error: fetchError || "Entity not found" }));
23998
+ } else {
23999
+ spinner.fail("Failed to fetch entity");
24000
+ console.log(source_default.red("Error:"), fetchError || "Entity not found");
24001
+ }
23480
24002
  process.exit(1);
23481
24003
  }
23482
- spinner.succeed("Entity loaded");
24004
+ if (!jsonMode)
24005
+ spinner.succeed("Entity loaded");
23483
24006
  const result = data;
23484
24007
  const entity = result.entity;
23485
24008
  const entityType = result.entityType;
23486
24009
  const entityData = entity.data;
23487
- console.log();
23488
- console.log(source_default.bold(` ${entityType.name}`), source_default.gray(`(${entity._id})`));
23489
- if (entityData) {
23490
- const preview = Object.entries(entityData).slice(0, 3);
23491
- for (const [key, value] of preview) {
23492
- console.log(` ${source_default.gray(key + ":")} ${String(value ?? "")}`);
24010
+ if (!jsonMode) {
24011
+ console.log();
24012
+ console.log(source_default.bold(` ${entityType.name}`), source_default.gray(`(${entity._id})`));
24013
+ if (entityData) {
24014
+ const preview = Object.entries(entityData).slice(0, 3);
24015
+ for (const [key, value] of preview) {
24016
+ console.log(` ${source_default.gray(key + ":")} ${String(value ?? "")}`);
24017
+ }
23493
24018
  }
24019
+ console.log();
23494
24020
  }
23495
- console.log();
23496
- if (!opts.yes) {
24021
+ if (!opts.yes && !jsonMode && isInteractive2()) {
23497
24022
  const confirmed = await esm_default2({
23498
24023
  message: "Are you sure you want to delete this entity?",
23499
24024
  default: false
@@ -23503,15 +24028,24 @@ entitiesCommand.command("delete <id>").description("Delete an entity").option("-
23503
24028
  return;
23504
24029
  }
23505
24030
  }
23506
- spinner.start("Deleting entity");
24031
+ if (!jsonMode)
24032
+ spinner.start("Deleting entity");
23507
24033
  const { error } = await removeEntity(id, env2);
23508
24034
  if (error) {
23509
- spinner.fail("Failed to delete entity");
23510
- console.log(source_default.red("Error:"), error);
24035
+ if (jsonMode) {
24036
+ console.log(JSON.stringify({ success: false, error }));
24037
+ } else {
24038
+ spinner.fail("Failed to delete entity");
24039
+ console.log(source_default.red("Error:"), error);
24040
+ }
23511
24041
  process.exit(1);
23512
24042
  }
23513
- spinner.succeed("Entity deleted");
23514
- console.log();
24043
+ if (jsonMode) {
24044
+ console.log(JSON.stringify({ success: true, id }));
24045
+ } else {
24046
+ spinner.succeed("Entity deleted");
24047
+ console.log();
24048
+ }
23515
24049
  });
23516
24050
  entitiesCommand.command("search <type> <query>").description("Search entities").option("--env <environment>", "Environment (development|production)", "development").option("--limit <n>", "Maximum results", "25").option("--json", "Output raw JSON").action(async (type, query, opts) => {
23517
24051
  await ensureAuth();
@@ -23791,7 +24325,7 @@ ${turn.assistantResponse}
23791
24325
  }
23792
24326
  return md;
23793
24327
  }
23794
- var runCommand = new Command("run").description("Run an eval suite").argument("<suite-slug>", "Eval suite slug to run").option("--case <name...>", "Run specific case(s) by name").option("--tag <tag...>", "Run cases matching tag(s)").action(async (suiteSlug, options) => {
24328
+ var runCommand = new Command("run").description("Run an eval suite").argument("<suite-slug>", "Eval suite slug to run").option("--case <name...>", "Run specific case(s) by name").option("--tag <tag...>", "Run cases matching tag(s)").option("--timeout <seconds>", "Max seconds to wait for results (default: 300)").action(async (suiteSlug, options) => {
23795
24329
  const spinner = ora();
23796
24330
  const cwd = process.cwd();
23797
24331
  console.log();
@@ -23925,6 +24459,9 @@ var runCommand = new Command("run").description("Run an eval suite").argument("<
23925
24459
  console.log(source_default.red("Failed to start run:"), error instanceof Error ? error.message : String(error));
23926
24460
  process.exit(1);
23927
24461
  }
24462
+ console.log(source_default.gray("Run ID:"), source_default.cyan(runId));
24463
+ const timeoutMs = (options.timeout ? parseInt(options.timeout, 10) : 300) * 1000;
24464
+ const startTime = Date.now();
23928
24465
  spinner.start("Running...");
23929
24466
  let run = null;
23930
24467
  while (true) {
@@ -23937,6 +24474,11 @@ var runCommand = new Command("run").description("Run an eval suite").argument("<
23937
24474
  if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") {
23938
24475
  break;
23939
24476
  }
24477
+ if (Date.now() - startTime > timeoutMs) {
24478
+ spinner.fail(`Timed out after ${options.timeout || 300}s \u2014 run is still in progress`);
24479
+ console.log(source_default.gray("Run ID:"), source_default.cyan(runId));
24480
+ process.exit(2);
24481
+ }
23940
24482
  spinner.text = `Running: ${run.completedCases}/${run.totalCases} cases (${run.passedCases} passed, ${run.failedCases} failed)`;
23941
24483
  }
23942
24484
  const duration = run.totalDurationMs ? formatDuration(run.totalDurationMs) : "-";
@@ -24094,7 +24636,12 @@ async function getTemplateStatus(connectionId, env2, name) {
24094
24636
  // src/cli/commands/templates.ts
24095
24637
  async function ensureAuth2() {
24096
24638
  const cwd = process.cwd();
24639
+ const nonInteractive = !isInteractive2();
24097
24640
  if (!hasProject(cwd)) {
24641
+ if (nonInteractive) {
24642
+ console.error(source_default.red("No struere.json found. Run struere init first."));
24643
+ process.exit(1);
24644
+ }
24098
24645
  console.log(source_default.yellow("No struere.json found - initializing project..."));
24099
24646
  console.log();
24100
24647
  const success = await runInit(cwd);
@@ -24106,6 +24653,10 @@ async function ensureAuth2() {
24106
24653
  let credentials = loadCredentials();
24107
24654
  const apiKey = getApiKey();
24108
24655
  if (!credentials && !apiKey) {
24656
+ if (nonInteractive) {
24657
+ console.error(source_default.red("Not authenticated. Set STRUERE_API_KEY or run struere login."));
24658
+ process.exit(1);
24659
+ }
24109
24660
  console.log(source_default.yellow("Not logged in - authenticating..."));
24110
24661
  console.log();
24111
24662
  credentials = await performLogin();
@@ -24255,7 +24806,7 @@ templatesCommand.command("delete <name>").description("Delete a message template
24255
24806
  await ensureAuth2();
24256
24807
  const env2 = opts.env;
24257
24808
  const connectionId = await resolveConnectionId(env2, opts.connection);
24258
- if (!opts.yes) {
24809
+ if (!opts.yes && isInteractive2()) {
24259
24810
  const confirmed = await esm_default2({
24260
24811
  message: `Delete template "${name}"? This cannot be undone.`,
24261
24812
  default: false
@@ -24314,7 +24865,7 @@ templatesCommand.command("status <name>").description("Check template approval s
24314
24865
  // package.json
24315
24866
  var package_default = {
24316
24867
  name: "struere",
24317
- version: "0.8.2",
24868
+ version: "0.9.0",
24318
24869
  description: "Build, test, and deploy AI agents",
24319
24870
  keywords: [
24320
24871
  "ai",
@@ -24380,6 +24931,8 @@ var CURRENT_VERSION = package_default.version;
24380
24931
  async function checkForUpdates() {
24381
24932
  if (process.env.STRUERE_SKIP_UPDATE_CHECK)
24382
24933
  return;
24934
+ if (process.env.STRUERE_API_KEY || !process.stdout.isTTY)
24935
+ return;
24383
24936
  try {
24384
24937
  const response = await fetch("https://registry.npmjs.org/struere/latest", {
24385
24938
  signal: AbortSignal.timeout(2000)
@@ -24399,9 +24952,12 @@ async function checkForUpdates() {
24399
24952
  return 0;
24400
24953
  };
24401
24954
  if (semverCompare(data.version, CURRENT_VERSION) > 0) {
24402
- console.log(`\x1B[33m\u26A0 Update available: ${CURRENT_VERSION} \u2192 ${data.version}\x1B[0m`);
24403
- console.log(`\x1B[90m Run: npm install -g struere@${data.version}\x1B[0m`);
24404
- console.log();
24955
+ process.stderr.write(`\x1B[33m\u26A0 Update available: ${CURRENT_VERSION} \u2192 ${data.version}\x1B[0m
24956
+ `);
24957
+ process.stderr.write(`\x1B[90m Run: npm install -g struere@${data.version}\x1B[0m
24958
+ `);
24959
+ process.stderr.write(`
24960
+ `);
24405
24961
  }
24406
24962
  }
24407
24963
  }
@@ -24413,6 +24969,7 @@ program.addCommand(initCommand);
24413
24969
  program.addCommand(loginCommand);
24414
24970
  program.addCommand(logoutCommand);
24415
24971
  program.addCommand(whoamiCommand);
24972
+ program.addCommand(syncCommand);
24416
24973
  program.addCommand(devCommand);
24417
24974
  program.addCommand(deployCommand);
24418
24975
  program.addCommand(addCommand);