@dura-run/cli 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/dura.js CHANGED
@@ -2262,9 +2262,20 @@ function formatObject(data) {
2262
2262
  }
2263
2263
 
2264
2264
  // src/lib/config-store.ts
2265
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2265
+ import {
2266
+ chmodSync,
2267
+ existsSync,
2268
+ mkdirSync,
2269
+ readFileSync,
2270
+ writeFileSync
2271
+ } from "node:fs";
2266
2272
  import { join } from "node:path";
2267
2273
  import { homedir } from "node:os";
2274
+ function chmodPosix(path, mode) {
2275
+ if (process.platform === "win32")
2276
+ return;
2277
+ chmodSync(path, mode);
2278
+ }
2268
2279
  function getConfigDir() {
2269
2280
  const explicitDir = process.env["DURA_CONFIG_DIR"];
2270
2281
  if (explicitDir)
@@ -2301,12 +2312,15 @@ function readConfig() {
2301
2312
  function writeConfig(config) {
2302
2313
  const dir = getConfigDir();
2303
2314
  if (!existsSync(dir)) {
2304
- mkdirSync(dir, { recursive: true });
2315
+ mkdirSync(dir, { recursive: true, mode: 448 });
2305
2316
  }
2306
- writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + `
2317
+ chmodPosix(dir, 448);
2318
+ const path = getConfigPath();
2319
+ writeFileSync(path, JSON.stringify(config, null, 2) + `
2307
2320
  `, {
2308
2321
  mode: 384
2309
2322
  });
2323
+ chmodPosix(path, 384);
2310
2324
  }
2311
2325
  function updateConfig(partial) {
2312
2326
  const current = readConfig();
@@ -3003,10 +3017,19 @@ var init_project_id = () => {};
3003
3017
  // src/commands/secrets.ts
3004
3018
  var exports_secrets = {};
3005
3019
  __export(exports_secrets, {
3006
- registerSecretsCommand: () => registerSecretsCommand
3020
+ registerSecretsCommand: () => registerSecretsCommand,
3021
+ escapeEnvValue: () => escapeEnvValue
3007
3022
  });
3008
- import { writeFileSync as writeFileSync3 } from "node:fs";
3023
+ import {
3024
+ writeFileSync as writeFileSync3,
3025
+ existsSync as existsSync4,
3026
+ readFileSync as readFileSync4,
3027
+ appendFileSync
3028
+ } from "node:fs";
3009
3029
  import { join as join4 } from "node:path";
3030
+ function escapeEnvValue(v) {
3031
+ return v.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
3032
+ }
3010
3033
  function resolveAuth(opts, output) {
3011
3034
  const projectId = resolveProjectId({ project: opts.project });
3012
3035
  if (!projectId) {
@@ -3066,28 +3089,65 @@ function registerSecretsCommand(program2) {
3066
3089
  reportApiError(output, err, "SECRET_REMOVE_FAILED");
3067
3090
  }
3068
3091
  });
3069
- secrets.command("pull").description("Write secrets to .env.local for local development").option("--project <id>", "Project ID (defaults to dura.json)").option("--api-url <url>", "API base URL").option("--token <token>", "Auth token").option("--dir <path>", "Directory to write .env.local to", ".").action(async (opts, cmd) => {
3092
+ secrets.command("pull").description("Write secrets to .env.local for local development (file holds DECRYPTED plaintext secrets)").option("--project <id>", "Project ID (defaults to dura.json)").option("--confirm", "Overwrite an existing .env.local").option("--api-url <url>", "API base URL").option("--token <token>", "Auth token").option("--dir <path>", "Directory to write .env.local to", ".").action(async (opts, cmd) => {
3070
3093
  const output = getOutput(cmd);
3094
+ const envPath = join4(opts.dir, ".env.local");
3095
+ if (existsSync4(envPath) && !opts.confirm) {
3096
+ output.error("FILE_EXISTS", `${envPath} already exists. Re-run with --confirm to overwrite.`);
3097
+ return;
3098
+ }
3071
3099
  const auth = resolveAuth(opts, output);
3072
3100
  if (!auth)
3073
3101
  return;
3074
3102
  const { client, projectId } = auth;
3075
3103
  try {
3076
3104
  const values = await client.get(`/api/v1/projects/${projectId}/secrets/pull`);
3077
- const lines = Object.entries(values).map(([k, v]) => `${k}="${v.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n")}"`);
3078
- const envPath = join4(opts.dir, ".env.local");
3105
+ const lines = Object.entries(values).map(([k, v]) => `${k}="${escapeEnvValue(v)}"`);
3079
3106
  writeFileSync3(envPath, lines.join(`
3080
3107
  `) + `
3081
3108
  `, { mode: 384 });
3109
+ let gitignore;
3110
+ try {
3111
+ gitignore = ensureGitignored(opts.dir) ? "added" : "present";
3112
+ } catch {
3113
+ gitignore = "failed";
3114
+ output.warn(`Could not update ${join4(opts.dir, ".gitignore")} — add ".env.local" to it yourself; the file holds plaintext secrets.`);
3115
+ }
3082
3116
  output.success({
3083
3117
  written: envPath,
3084
- count: Object.keys(values).length
3118
+ count: Object.keys(values).length,
3119
+ gitignore
3085
3120
  });
3086
3121
  } catch (err) {
3087
3122
  reportApiError(output, err, "SECRET_PULL_FAILED");
3088
3123
  }
3089
3124
  });
3090
3125
  }
3126
+ function ensureGitignored(dir) {
3127
+ const gitignorePath = join4(dir, ".gitignore");
3128
+ const covers = new Set([
3129
+ ".env.local",
3130
+ "/.env.local",
3131
+ ".env.local/",
3132
+ ".env*",
3133
+ ".env.*",
3134
+ "*.local"
3135
+ ]);
3136
+ let existing = "";
3137
+ if (existsSync4(gitignorePath)) {
3138
+ existing = readFileSync4(gitignorePath, "utf-8");
3139
+ const alreadyCovered = existing.split(`
3140
+ `).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).some((line) => covers.has(line));
3141
+ if (alreadyCovered)
3142
+ return false;
3143
+ }
3144
+ const prefix = existing.length > 0 && !existing.endsWith(`
3145
+ `) ? `
3146
+ ` : "";
3147
+ appendFileSync(gitignorePath, `${prefix}.env.local
3148
+ `);
3149
+ return true;
3150
+ }
3091
3151
  var init_secrets = __esm(() => {
3092
3152
  init_src3();
3093
3153
  init_api_client();
@@ -3096,6 +3156,9 @@ var init_secrets = __esm(() => {
3096
3156
  init_project_id();
3097
3157
  });
3098
3158
 
3159
+ // ../shared/src/types/queue.ts
3160
+ var init_queue = () => {};
3161
+
3099
3162
  // ../shared/src/constants/defaults.ts
3100
3163
  var DEFAULT_TIMEOUT_MS = 30000, DEFAULT_MEMORY_LIMIT_MB = 128;
3101
3164
  // ../shared/src/constants/billing.ts
@@ -7272,15 +7335,15 @@ var init_marketplace = __esm(() => {
7272
7335
  readme: exports_external.string().max(50000).optional()
7273
7336
  }).strict();
7274
7337
  installAdapterSchema = exports_external.object({
7275
- adapterId: exports_external.string().min(1, "Adapter ID is required"),
7338
+ adapterId: exports_external.string().uuid("Adapter ID must be a valid UUID"),
7276
7339
  config: exports_external.record(exports_external.unknown()).optional()
7277
7340
  });
7278
7341
  searchAdaptersSchema = exports_external.object({
7279
7342
  query: exports_external.string().optional(),
7280
7343
  category: exports_external.string().optional(),
7281
7344
  status: exports_external.enum(["draft", "published", "deprecated"]).optional(),
7282
- limit: exports_external.number().int().positive().max(100).default(20),
7283
- offset: exports_external.number().int().nonnegative().default(0)
7345
+ limit: exports_external.coerce.number().int().positive().max(100).default(20),
7346
+ offset: exports_external.coerce.number().int().nonnegative().default(0)
7284
7347
  });
7285
7348
  });
7286
7349
 
@@ -7787,7 +7850,7 @@ function Queue(initial = []) {
7787
7850
  };
7788
7851
  }
7789
7852
  var queue_default;
7790
- var init_queue = __esm(() => {
7853
+ var init_queue2 = __esm(() => {
7791
7854
  queue_default = Queue;
7792
7855
  });
7793
7856
 
@@ -8565,7 +8628,7 @@ var init_connection = __esm(() => {
8565
8628
  init_types2();
8566
8629
  init_errors2();
8567
8630
  init_result();
8568
- init_queue();
8631
+ init_queue2();
8569
8632
  init_query();
8570
8633
  init_bytes();
8571
8634
  connection_default = Connection;
@@ -9245,7 +9308,7 @@ var init_src = __esm(() => {
9245
9308
  init_types2();
9246
9309
  init_connection();
9247
9310
  init_query();
9248
- init_queue();
9311
+ init_queue2();
9249
9312
  init_errors2();
9250
9313
  init_large();
9251
9314
  Object.assign(Postgres, {
@@ -12789,7 +12852,8 @@ var init_deployments = __esm(() => {
12789
12852
  index("deployments_project_idx").on(table3.projectId),
12790
12853
  index("deployments_project_status_idx").on(table3.projectId, table3.status),
12791
12854
  index("deployments_environment_idx").on(table3.environmentId),
12792
- index("deployments_project_created_idx").on(table3.projectId, desc(table3.createdAt))
12855
+ index("deployments_project_created_idx").on(table3.projectId, desc(table3.createdAt)),
12856
+ uniqueIndex("deployments_one_active_per_project").on(table3.projectId).where(sql`${table3.status} = 'active'`)
12793
12857
  ]);
12794
12858
  });
12795
12859
 
@@ -12894,6 +12958,32 @@ var init_metrics = __esm(() => {
12894
12958
  ]);
12895
12959
  });
12896
12960
 
12961
+ // ../shared/src/db/schema/spans.ts
12962
+ var spans;
12963
+ var init_spans = __esm(() => {
12964
+ init_pg_core();
12965
+ init_execution_records();
12966
+ init_projects();
12967
+ spans = pgTable("spans", {
12968
+ id: uuid("id").defaultRandom().primaryKey(),
12969
+ executionId: uuid("execution_id").notNull().references(() => executionRecords.id, { onDelete: "cascade" }),
12970
+ projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
12971
+ traceId: text("trace_id").notNull(),
12972
+ spanId: text("span_id").notNull(),
12973
+ parentSpanId: text("parent_span_id"),
12974
+ name: text("name").notNull(),
12975
+ kind: text("kind").notNull(),
12976
+ startedAt: timestamp("started_at", { withTimezone: true }).notNull(),
12977
+ endedAt: timestamp("ended_at", { withTimezone: true }).notNull(),
12978
+ attributes: jsonb("attributes").$type(),
12979
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
12980
+ }, (table3) => [
12981
+ index("spans_execution_idx").on(table3.executionId),
12982
+ index("spans_project_idx").on(table3.projectId),
12983
+ index("spans_trace_idx").on(table3.traceId)
12984
+ ]);
12985
+ });
12986
+
12897
12987
  // ../shared/src/db/schema/artifacts.ts
12898
12988
  var artifacts;
12899
12989
  var init_artifacts = __esm(() => {
@@ -13091,6 +13181,7 @@ var init_credit_ledger = __esm(() => {
13091
13181
  }, (table3) => [
13092
13182
  index("credit_ledger_org_created_idx").on(table3.organizationId, table3.createdAt),
13093
13183
  uniqueIndex("credit_ledger_execution_debit_dedup").on(sql`((metadata->>'executionId'))`).where(sql`entry_type IN ('execution_debit', 'overage_debit') AND metadata ? 'executionId'`),
13184
+ uniqueIndex("credit_ledger_execution_refund_dedup").on(sql`((metadata->>'executionId'))`).where(sql`entry_type = 'execution_refund' AND metadata ? 'executionId'`),
13094
13185
  uniqueIndex("credit_ledger_included_grant_dedup").on(table3.organizationId, sql`((metadata->>'billingCycle'))`, sql`((metadata->>'plan'))`).where(sql`entry_type = 'included_grant' AND metadata ? 'billingCycle' AND metadata ? 'plan'`)
13095
13186
  ]);
13096
13187
  });
@@ -13412,12 +13503,14 @@ var init_workflows = __esm(() => {
13412
13503
  ]);
13413
13504
  stepRuns = pgTable("step_runs", {
13414
13505
  id: uuid("id").defaultRandom().primaryKey(),
13415
- workflowRunId: uuid("workflow_run_id").notNull().references(() => workflowRuns.id),
13506
+ workflowRunId: uuid("workflow_run_id").notNull().references(() => workflowRuns.id, { onDelete: "cascade" }),
13416
13507
  stepName: text("step_name").notNull(),
13417
13508
  status: text("status").notNull().default("pending"),
13418
13509
  input: jsonb("input"),
13419
13510
  output: jsonb("output"),
13420
- executionId: uuid("execution_id").references(() => executionRecords.id),
13511
+ executionId: uuid("execution_id").references(() => executionRecords.id, {
13512
+ onDelete: "set null"
13513
+ }),
13421
13514
  attempt: integer("attempt").notNull().default(1),
13422
13515
  startedAt: timestamp("started_at", { withTimezone: true }),
13423
13516
  completedAt: timestamp("completed_at", { withTimezone: true }),
@@ -13430,7 +13523,7 @@ var init_workflows = __esm(() => {
13430
13523
  ]);
13431
13524
  approvalRequests = pgTable("approval_requests", {
13432
13525
  id: uuid("id").defaultRandom().primaryKey(),
13433
- stepRunId: uuid("step_run_id").notNull().references(() => stepRuns.id),
13526
+ stepRunId: uuid("step_run_id").notNull().references(() => stepRuns.id, { onDelete: "cascade" }),
13434
13527
  approvers: jsonb("approvers").$type().notNull(),
13435
13528
  message: text("message"),
13436
13529
  status: text("status").notNull().default("pending"),
@@ -13732,6 +13825,27 @@ var init_admin_audit_log = __esm(() => {
13732
13825
  ]);
13733
13826
  });
13734
13827
 
13828
+ // ../shared/src/db/schema/custom-domains.ts
13829
+ var customDomains;
13830
+ var init_custom_domains = __esm(() => {
13831
+ init_pg_core();
13832
+ init_projects();
13833
+ customDomains = pgTable("custom_domains", {
13834
+ id: uuid("id").defaultRandom().primaryKey(),
13835
+ projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
13836
+ domain: text("domain").notNull(),
13837
+ status: text("status").notNull(),
13838
+ txtRecord: text("txt_record").notNull(),
13839
+ verifiedAt: timestamp("verified_at", { withTimezone: true }),
13840
+ tlsProvisionedAt: timestamp("tls_provisioned_at", { withTimezone: true }),
13841
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
13842
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
13843
+ }, (table3) => [
13844
+ uniqueIndex("custom_domains_domain_idx").on(table3.domain),
13845
+ index("custom_domains_project_idx").on(table3.projectId)
13846
+ ]);
13847
+ });
13848
+
13735
13849
  // ../shared/src/db/schema/index.ts
13736
13850
  var init_schema2 = __esm(() => {
13737
13851
  init_users();
@@ -13744,6 +13858,7 @@ var init_schema2 = __esm(() => {
13744
13858
  init_execution_records();
13745
13859
  init_log_entries();
13746
13860
  init_metrics();
13861
+ init_spans();
13747
13862
  init_artifacts();
13748
13863
  init_secrets2();
13749
13864
  init_api_keys();
@@ -13768,6 +13883,7 @@ var init_schema2 = __esm(() => {
13768
13883
  init_auth();
13769
13884
  init_admin_sessions();
13770
13885
  init_admin_audit_log();
13886
+ init_custom_domains();
13771
13887
  });
13772
13888
 
13773
13889
  // ../shared/src/db/index.ts
@@ -13800,6 +13916,7 @@ function parseHttpBody(body, _headers) {
13800
13916
 
13801
13917
  // ../shared/src/index.ts
13802
13918
  var init_src2 = __esm(() => {
13919
+ init_queue();
13803
13920
  init_billing();
13804
13921
  init_limits();
13805
13922
  init_native_deps();
@@ -13813,16 +13930,16 @@ var init_src2 = __esm(() => {
13813
13930
  });
13814
13931
 
13815
13932
  // src/lib/manifest.ts
13816
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
13933
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
13817
13934
  import { join as join5 } from "node:path";
13818
13935
  function readManifest(dir) {
13819
13936
  const path = join5(dir, "dura.json");
13820
- if (!existsSync4(path)) {
13937
+ if (!existsSync5(path)) {
13821
13938
  throw new ManifestError(`No dura.json found in ${dir}`);
13822
13939
  }
13823
13940
  let raw;
13824
13941
  try {
13825
- raw = JSON.parse(readFileSync4(path, "utf-8"));
13942
+ raw = JSON.parse(readFileSync5(path, "utf-8"));
13826
13943
  } catch {
13827
13944
  throw new ManifestError("dura.json is not valid JSON");
13828
13945
  }
@@ -13834,10 +13951,10 @@ function readManifest(dir) {
13834
13951
  }
13835
13952
  function readRawManifest(dir) {
13836
13953
  const path = join5(dir, "dura.json");
13837
- if (!existsSync4(path)) {
13954
+ if (!existsSync5(path)) {
13838
13955
  throw new ManifestError(`No dura.json found in ${dir}`);
13839
13956
  }
13840
- return JSON.parse(readFileSync4(path, "utf-8"));
13957
+ return JSON.parse(readFileSync5(path, "utf-8"));
13841
13958
  }
13842
13959
  function writeRawManifest(dir, data) {
13843
13960
  const path = join5(dir, "dura.json");
@@ -14078,8 +14195,8 @@ var init_parity_check = __esm(() => {
14078
14195
  // src/lib/bundler.ts
14079
14196
  import { join as join6, resolve as resolve2 } from "node:path";
14080
14197
  import {
14081
- existsSync as existsSync5,
14082
- readFileSync as readFileSync5,
14198
+ existsSync as existsSync6,
14199
+ readFileSync as readFileSync6,
14083
14200
  writeFileSync as writeFileSync5,
14084
14201
  mkdirSync as mkdirSync3,
14085
14202
  readdirSync,
@@ -14100,14 +14217,14 @@ async function createBundle(projectDir, options) {
14100
14217
  };
14101
14218
  }
14102
14219
  const buildDir = join6(projectDir, ".dura", "build");
14103
- if (existsSync5(buildDir)) {
14220
+ if (existsSync6(buildDir)) {
14104
14221
  rmSync(buildDir, { recursive: true });
14105
14222
  }
14106
14223
  mkdirSync3(buildDir, { recursive: true });
14107
14224
  const parityWarnings = [];
14108
14225
  for (const auto of duraManifest.automations) {
14109
14226
  const entryPath = resolve2(projectDir, auto.entrypoint);
14110
- if (!existsSync5(entryPath)) {
14227
+ if (!existsSync6(entryPath)) {
14111
14228
  rmSync(buildDir, { recursive: true });
14112
14229
  return {
14113
14230
  success: false,
@@ -14116,7 +14233,7 @@ async function createBundle(projectDir, options) {
14116
14233
  }
14117
14234
  const outFile = join6(buildDir, `${auto.name}.js`);
14118
14235
  try {
14119
- const source = readFileSync5(entryPath, "utf-8");
14236
+ const source = readFileSync6(entryPath, "utf-8");
14120
14237
  const xformed = await esbuild2.transform(source, {
14121
14238
  loader: "ts",
14122
14239
  target: "es2022",
@@ -14187,7 +14304,7 @@ function collectFiles(dir, base) {
14187
14304
  const relativePath = fullPath.slice(base.length + 1);
14188
14305
  result.push({
14189
14306
  name: relativePath,
14190
- content: new Uint8Array(readFileSync5(fullPath))
14307
+ content: new Uint8Array(readFileSync6(fullPath))
14191
14308
  });
14192
14309
  } else if (entry.isDirectory()) {
14193
14310
  result.push(...collectFiles(fullPath, base));
@@ -14415,12 +14532,13 @@ __export(exports_logs, {
14415
14532
  parseFilters: () => parseFilters,
14416
14533
  parseDuration: () => parseDuration,
14417
14534
  formatLogEntry: () => formatLogEntry,
14535
+ fetchWsTicket: () => fetchWsTicket,
14418
14536
  createFollowClient: () => createFollowClient,
14419
14537
  buildWsUrl: () => buildWsUrl
14420
14538
  });
14421
- function buildWsUrl(apiUrl, projectId, filters, token = "") {
14539
+ function buildWsUrl(apiUrl, projectId, filters, ticket = "") {
14422
14540
  const wsBase = apiUrl.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://");
14423
- const params = new URLSearchParams({ token, projectId });
14541
+ const params = new URLSearchParams({ ticket, projectId });
14424
14542
  if (filters.level)
14425
14543
  params.set("level", filters.level);
14426
14544
  if (filters.executionId)
@@ -14429,6 +14547,10 @@ function buildWsUrl(apiUrl, projectId, filters, token = "") {
14429
14547
  params.set("automationName", filters.automationName);
14430
14548
  return `${wsBase}/api/v1/ws?${params.toString()}`;
14431
14549
  }
14550
+ async function fetchWsTicket(client) {
14551
+ const res = await client.post("/api/v1/logs/ws-ticket");
14552
+ return res.ticket;
14553
+ }
14432
14554
  function formatLogEntry(entry) {
14433
14555
  const fieldsStr = entry.fields && Object.keys(entry.fields).length > 0 ? " " + JSON.stringify(entry.fields) : "";
14434
14556
  return `${entry.timestamp} [${entry.level.toUpperCase()}] ${entry.message}${fieldsStr}`;
@@ -14530,7 +14652,14 @@ function registerLogsCommand(program2) {
14530
14652
  }
14531
14653
  if (executionId)
14532
14654
  filters.executionId = executionId;
14533
- const wsUrl = buildWsUrl(apiUrl, projectId, filters, token);
14655
+ let ticket;
14656
+ try {
14657
+ ticket = await fetchWsTicket(client);
14658
+ } catch (err) {
14659
+ reportApiError(output, err, "WS_TICKET_FAILED");
14660
+ return;
14661
+ }
14662
+ const wsUrl = buildWsUrl(apiUrl, projectId, filters, ticket);
14534
14663
  if (!output.isJson()) {
14535
14664
  output.info(`Connecting to live log stream for project ${projectId}…`);
14536
14665
  }
@@ -14811,9 +14940,20 @@ var init_schedule = __esm(() => {
14811
14940
  });
14812
14941
 
14813
14942
  // src/lib/dev-trust.ts
14814
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "node:fs";
14943
+ import { chmodSync as chmodSync2, existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "node:fs";
14815
14944
  import { dirname, join as join7 } from "node:path";
14945
+ function chmodPosix2(path, mode) {
14946
+ if (process.platform === "win32")
14947
+ return;
14948
+ chmodSync2(path, mode);
14949
+ }
14816
14950
  function hasStoredCredentials() {
14951
+ const envToken = process.env["DURA_TOKEN"];
14952
+ if (typeof envToken === "string" && envToken.length > 0)
14953
+ return true;
14954
+ const envApiKey = process.env["DURA_API_KEY"];
14955
+ if (typeof envApiKey === "string" && envApiKey.length > 0)
14956
+ return true;
14817
14957
  try {
14818
14958
  const cfg = readConfig();
14819
14959
  const apiKey = typeof cfg.apiKey === "string" ? cfg.apiKey : "";
@@ -14824,14 +14964,15 @@ function hasStoredCredentials() {
14824
14964
  }
14825
14965
  }
14826
14966
  function hasProjectTrustMarker(projectDir) {
14827
- return existsSync6(join7(projectDir, TRUST_MARKER_RELATIVE_PATH));
14967
+ return existsSync7(join7(projectDir, TRUST_MARKER_RELATIVE_PATH));
14828
14968
  }
14829
14969
  function grantProjectTrust(projectDir) {
14830
14970
  const markerPath = join7(projectDir, TRUST_MARKER_RELATIVE_PATH);
14831
14971
  const dir = dirname(markerPath);
14832
- if (!existsSync6(dir)) {
14833
- mkdirSync4(dir, { recursive: true });
14972
+ if (!existsSync7(dir)) {
14973
+ mkdirSync4(dir, { recursive: true, mode: 448 });
14834
14974
  }
14975
+ chmodPosix2(dir, 448);
14835
14976
  const note = [
14836
14977
  "# dura dev trust marker",
14837
14978
  "#",
@@ -14843,6 +14984,7 @@ function grantProjectTrust(projectDir) {
14843
14984
  ].join(`
14844
14985
  `);
14845
14986
  writeFileSync6(markerPath, note, { mode: 384 });
14987
+ chmodPosix2(markerPath, 384);
14846
14988
  }
14847
14989
  function isEnvTruthy(val) {
14848
14990
  if (!val)
@@ -15445,7 +15587,7 @@ __export(exports_file_watcher, {
15445
15587
  resolveAffectedAutomation: () => resolveAffectedAutomation,
15446
15588
  createFileWatcher: () => createFileWatcher
15447
15589
  });
15448
- import { watch, existsSync as existsSync7 } from "node:fs";
15590
+ import { watch, existsSync as existsSync8 } from "node:fs";
15449
15591
  import { join as join8 } from "node:path";
15450
15592
  function resolveAffectedAutomation(manifest, changedPath) {
15451
15593
  const normalized = changedPath.replace(/\\/g, "/");
@@ -15482,7 +15624,7 @@ function createFileWatcher(options) {
15482
15624
  watching = true;
15483
15625
  for (const dir of watchDirs) {
15484
15626
  const fullDir = join8(projectDir, dir);
15485
- if (!existsSync7(fullDir))
15627
+ if (!existsSync8(fullDir))
15486
15628
  continue;
15487
15629
  try {
15488
15630
  const fsWatcher = watch(fullDir, { recursive: true }, (_eventType, filename) => {
@@ -15533,7 +15675,10 @@ function parseEnvFile(content) {
15533
15675
  }
15534
15676
  const key = line3.slice(0, eqIndex).trim();
15535
15677
  let value = line3.slice(eqIndex + 1).trim();
15536
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
15678
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
15679
+ value = value.slice(1, -1).replace(/\\(.)/g, (_, c) => c === "n" ? `
15680
+ ` : c);
15681
+ } else if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
15537
15682
  value = value.slice(1, -1);
15538
15683
  }
15539
15684
  if (key.length > 0) {
@@ -15594,12 +15739,12 @@ function registerDevCommand(program2) {
15594
15739
  }
15595
15740
  throw err;
15596
15741
  }
15597
- const { existsSync: existsSync8, readFileSync: readFileSync6 } = await import("node:fs");
15742
+ const { existsSync: existsSync9, readFileSync: readFileSync7 } = await import("node:fs");
15598
15743
  const { join: join9 } = await import("node:path");
15599
15744
  const envPath = join9(projectDir, ".env.local");
15600
15745
  let secrets2 = {};
15601
- if (existsSync8(envPath)) {
15602
- const content = readFileSync6(envPath, "utf-8");
15746
+ if (existsSync9(envPath)) {
15747
+ const content = readFileSync7(envPath, "utf-8");
15603
15748
  secrets2 = parseEnvFile(content);
15604
15749
  output.info(`Loaded ${Object.keys(secrets2).length} secret(s) from .env.local`);
15605
15750
  }
@@ -15950,9 +16095,9 @@ var init_comparator = __esm(() => {
15950
16095
 
15951
16096
  // src/snapshot/file-manager.ts
15952
16097
  import {
15953
- existsSync as existsSync8,
16098
+ existsSync as existsSync9,
15954
16099
  mkdirSync as mkdirSync5,
15955
- readFileSync as readFileSync6,
16100
+ readFileSync as readFileSync7,
15956
16101
  writeFileSync as writeFileSync7,
15957
16102
  readdirSync as readdirSync2,
15958
16103
  unlinkSync
@@ -15974,16 +16119,16 @@ class SnapshotFileManager {
15974
16119
  return join9(this.snapshotsDir, automationNameToFileName(automationName));
15975
16120
  }
15976
16121
  ensureDir() {
15977
- if (!existsSync8(this.snapshotsDir)) {
16122
+ if (!existsSync9(this.snapshotsDir)) {
15978
16123
  mkdirSync5(this.snapshotsDir, { recursive: true });
15979
16124
  }
15980
16125
  }
15981
16126
  read(automationName) {
15982
16127
  const path = this.filePath(automationName);
15983
- if (!existsSync8(path))
16128
+ if (!existsSync9(path))
15984
16129
  return null;
15985
16130
  try {
15986
- const raw = readFileSync6(path, "utf-8");
16131
+ const raw = readFileSync7(path, "utf-8");
15987
16132
  return JSON.parse(raw);
15988
16133
  } catch {
15989
16134
  return null;
@@ -16013,14 +16158,14 @@ class SnapshotFileManager {
16013
16158
  });
16014
16159
  }
16015
16160
  listAutomationNames() {
16016
- if (!existsSync8(this.snapshotsDir))
16161
+ if (!existsSync9(this.snapshotsDir))
16017
16162
  return [];
16018
16163
  const files = readdirSync2(this.snapshotsDir).filter((f) => f.endsWith(".snap.json"));
16019
16164
  const names = [];
16020
16165
  for (const file of files) {
16021
16166
  const path = join9(this.snapshotsDir, file);
16022
16167
  try {
16023
- const raw = readFileSync6(path, "utf-8");
16168
+ const raw = readFileSync7(path, "utf-8");
16024
16169
  const parsed = JSON.parse(raw);
16025
16170
  if (parsed.automationName) {
16026
16171
  names.push(parsed.automationName);
@@ -16031,7 +16176,7 @@ class SnapshotFileManager {
16031
16176
  }
16032
16177
  delete(automationName) {
16033
16178
  const path = this.filePath(automationName);
16034
- if (existsSync8(path)) {
16179
+ if (existsSync9(path)) {
16035
16180
  unlinkSync(path);
16036
16181
  }
16037
16182
  }
@@ -16040,7 +16185,7 @@ var SNAPSHOTS_DIR = "__snapshots__";
16040
16185
  var init_file_manager = () => {};
16041
16186
 
16042
16187
  // src/snapshot/config-reader.ts
16043
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
16188
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
16044
16189
  import { join as join10 } from "node:path";
16045
16190
  function readSnapshotConfig(projectDir) {
16046
16191
  const durajsonPath = join10(projectDir, "dura.json");
@@ -16048,17 +16193,17 @@ function readSnapshotConfig(projectDir) {
16048
16193
  let fromDuraJson = {};
16049
16194
  let fromTestJson = {};
16050
16195
  let mocks;
16051
- if (existsSync9(durajsonPath)) {
16196
+ if (existsSync10(durajsonPath)) {
16052
16197
  try {
16053
- const raw = JSON.parse(readFileSync7(durajsonPath, "utf-8"));
16198
+ const raw = JSON.parse(readFileSync8(durajsonPath, "utf-8"));
16054
16199
  if (raw["snapshots"] && typeof raw["snapshots"] === "object" && !Array.isArray(raw["snapshots"])) {
16055
16200
  fromDuraJson = raw["snapshots"];
16056
16201
  }
16057
16202
  } catch {}
16058
16203
  }
16059
- if (existsSync9(testjsonPath)) {
16204
+ if (existsSync10(testjsonPath)) {
16060
16205
  try {
16061
- const raw = JSON.parse(readFileSync7(testjsonPath, "utf-8"));
16206
+ const raw = JSON.parse(readFileSync8(testjsonPath, "utf-8"));
16062
16207
  if (raw["snapshots"] && typeof raw["snapshots"] === "object" && !Array.isArray(raw["snapshots"])) {
16063
16208
  fromTestJson = raw["snapshots"];
16064
16209
  }
@@ -16808,7 +16953,7 @@ __export(exports_events, {
16808
16953
  resolvePayload: () => resolvePayload,
16809
16954
  registerEventsCommand: () => registerEventsCommand
16810
16955
  });
16811
- import { readFileSync as readFileSync8 } from "node:fs";
16956
+ import { readFileSync as readFileSync9 } from "node:fs";
16812
16957
  function resolvePayload(opts) {
16813
16958
  if (opts.payload !== undefined && opts.payloadFile !== undefined) {
16814
16959
  throw new Error("Use either --payload or --payload-file, not both");
@@ -16816,7 +16961,7 @@ function resolvePayload(opts) {
16816
16961
  if (opts.payload === undefined && opts.payloadFile === undefined) {
16817
16962
  throw new Error("--payload or --payload-file is required");
16818
16963
  }
16819
- const raw = opts.payload !== undefined ? opts.payload : readFileSync8(opts.payloadFile, "utf-8");
16964
+ const raw = opts.payload !== undefined ? opts.payload : readFileSync9(opts.payloadFile, "utf-8");
16820
16965
  try {
16821
16966
  return JSON.parse(raw);
16822
16967
  } catch (err) {
@@ -17102,8 +17247,8 @@ __export(exports_export, {
17102
17247
  collectExportFiles: () => collectExportFiles
17103
17248
  });
17104
17249
  import {
17105
- existsSync as existsSync10,
17106
- readFileSync as readFileSync9,
17250
+ existsSync as existsSync11,
17251
+ readFileSync as readFileSync10,
17107
17252
  readdirSync as readdirSync3,
17108
17253
  statSync,
17109
17254
  writeFileSync as writeFileSync8
@@ -17123,7 +17268,7 @@ function collectExportFiles(baseDir, currentDir) {
17123
17268
  }
17124
17269
  } else if (stat.isFile()) {
17125
17270
  if (!EXCLUDED_FILES.has(item)) {
17126
- const content = readFileSync9(fullPath, "utf-8");
17271
+ const content = readFileSync10(fullPath, "utf-8");
17127
17272
  entries.push({ relativePath: relPath, content });
17128
17273
  }
17129
17274
  }
@@ -17148,13 +17293,13 @@ function registerExportCommand(program2) {
17148
17293
  const output = getOutput(cmd);
17149
17294
  const projectDir = resolve4(opts.dir ?? ".");
17150
17295
  const manifestPath = join11(projectDir, "dura.json");
17151
- if (!existsSync10(manifestPath)) {
17296
+ if (!existsSync11(manifestPath)) {
17152
17297
  output.error("EXPORT_NO_MANIFEST", "No dura.json found in project directory", "Run this command from a dura project directory or use --dir");
17153
17298
  return;
17154
17299
  }
17155
17300
  let projectName;
17156
17301
  try {
17157
- const manifestContent = readFileSync9(manifestPath, "utf-8");
17302
+ const manifestContent = readFileSync10(manifestPath, "utf-8");
17158
17303
  const manifest = JSON.parse(manifestContent);
17159
17304
  projectName = manifest.name ?? "unnamed-project";
17160
17305
  } catch {
@@ -18021,17 +18166,18 @@ var init_diagnose = __esm(() => {
18021
18166
  // src/commands/create.ts
18022
18167
  var exports_create = {};
18023
18168
  __export(exports_create, {
18169
+ writeGeneratedFiles: () => writeGeneratedFiles,
18024
18170
  registerCreateCommand: () => registerCreateCommand,
18025
18171
  registerAddCommand: () => registerAddCommand
18026
18172
  });
18027
18173
  import {
18028
- existsSync as existsSync11,
18174
+ existsSync as existsSync12,
18029
18175
  mkdirSync as mkdirSync6,
18030
18176
  writeFileSync as writeFileSync9,
18031
- readFileSync as readFileSync10,
18177
+ readFileSync as readFileSync11,
18032
18178
  readdirSync as readdirSync4
18033
18179
  } from "node:fs";
18034
- import { join as join12, resolve as resolve5, dirname as dirname2 } from "node:path";
18180
+ import { join as join12, resolve as resolve5, dirname as dirname2, isAbsolute as isAbsolute2, sep } from "node:path";
18035
18181
  function slugifyDescription(description) {
18036
18182
  return description.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 30).replace(/-+$/, "");
18037
18183
  }
@@ -18039,13 +18185,13 @@ function getExistingRoutes(projectDir) {
18039
18185
  const routesDir = join12(projectDir, "routes");
18040
18186
  const jobsDir = join12(projectDir, "jobs");
18041
18187
  const routes = [];
18042
- if (existsSync11(routesDir)) {
18188
+ if (existsSync12(routesDir)) {
18043
18189
  try {
18044
18190
  const files = readdirSync4(routesDir);
18045
18191
  routes.push(...files.map((f) => `routes/${f}`));
18046
18192
  } catch {}
18047
18193
  }
18048
- if (existsSync11(jobsDir)) {
18194
+ if (existsSync12(jobsDir)) {
18049
18195
  try {
18050
18196
  const files = readdirSync4(jobsDir);
18051
18197
  routes.push(...files.map((f) => `jobs/${f}`));
@@ -18054,13 +18200,26 @@ function getExistingRoutes(projectDir) {
18054
18200
  return routes;
18055
18201
  }
18056
18202
  function writeGeneratedFiles(projectDir, files) {
18057
- for (const file of files) {
18058
- const filePath = join12(projectDir, file.path);
18059
- const dir = dirname2(filePath);
18060
- if (!existsSync11(dir)) {
18203
+ const root = resolve5(projectDir);
18204
+ const resolved = files.map((file) => {
18205
+ if (isAbsolute2(file.path)) {
18206
+ throw new Error(`Refusing to write generated file with absolute path "${file.path}" — paths must be relative to the project directory.`);
18207
+ }
18208
+ if (file.path.split(/[\\/]/).includes("..")) {
18209
+ throw new Error(`Refusing to write generated file "${file.path}" — path traversal ("..") is not allowed.`);
18210
+ }
18211
+ const abs = resolve5(root, file.path);
18212
+ if (abs !== root && !abs.startsWith(root + sep)) {
18213
+ throw new Error(`Refusing to write generated file "${file.path}" — resolved path "${abs}" is outside the project directory.`);
18214
+ }
18215
+ return { abs, content: file.content };
18216
+ });
18217
+ for (const { abs, content } of resolved) {
18218
+ const dir = dirname2(abs);
18219
+ if (!existsSync12(dir)) {
18061
18220
  mkdirSync6(dir, { recursive: true });
18062
18221
  }
18063
- writeFileSync9(filePath, file.content, "utf-8");
18222
+ writeFileSync9(abs, content, "utf-8");
18064
18223
  }
18065
18224
  }
18066
18225
  async function confirm(question) {
@@ -18090,7 +18249,7 @@ function registerCreateCommand(program2) {
18090
18249
  const projectName = opts.name || slugifyDescription(description);
18091
18250
  const parentDir = opts.dir ? resolve5(opts.dir) : process.cwd();
18092
18251
  const projectDir = join12(parentDir, projectName);
18093
- if (existsSync11(projectDir)) {
18252
+ if (existsSync12(projectDir)) {
18094
18253
  output.error("DIRECTORY_EXISTS", `Directory "${projectName}" already exists`, "Choose a different name with --name or remove the existing directory");
18095
18254
  return;
18096
18255
  }
@@ -18117,7 +18276,12 @@ function registerCreateCommand(program2) {
18117
18276
  }
18118
18277
  mkdirSync6(join12(projectDir, "routes"), { recursive: true });
18119
18278
  mkdirSync6(join12(projectDir, "jobs"), { recursive: true });
18120
- writeGeneratedFiles(projectDir, generateResult.files);
18279
+ try {
18280
+ writeGeneratedFiles(projectDir, generateResult.files);
18281
+ } catch (err) {
18282
+ output.error("UNSAFE_GENERATED_PATH", err instanceof Error ? err.message : "Unsafe generated file path", "The AI response contained a file path outside the project directory and was rejected.");
18283
+ return;
18284
+ }
18121
18285
  output.info(`Files written to ${projectDir}`);
18122
18286
  if (opts.deploy !== false) {
18123
18287
  output.info("Deploying...");
@@ -18148,14 +18312,14 @@ function registerAddCommand(program2) {
18148
18312
  }
18149
18313
  const projectDir = opts.dir ? resolve5(opts.dir) : process.cwd();
18150
18314
  const duraJsonPath = join12(projectDir, "dura.json");
18151
- if (!existsSync11(duraJsonPath)) {
18315
+ if (!existsSync12(duraJsonPath)) {
18152
18316
  output.error("NOT_A_DURA_PROJECT", "No dura.json found in the current directory", "Run this command from a dura project directory or use --dir");
18153
18317
  return;
18154
18318
  }
18155
18319
  const existingRoutes = getExistingRoutes(projectDir);
18156
18320
  let projectConfig = {};
18157
18321
  try {
18158
- projectConfig = JSON.parse(readFileSync10(duraJsonPath, "utf-8"));
18322
+ projectConfig = JSON.parse(readFileSync11(duraJsonPath, "utf-8"));
18159
18323
  } catch {}
18160
18324
  const apiUrl = getApiUrl();
18161
18325
  const client = new ApiClient(apiUrl, token);
@@ -18184,7 +18348,12 @@ function registerAddCommand(program2) {
18184
18348
  return;
18185
18349
  }
18186
18350
  }
18187
- writeGeneratedFiles(projectDir, generateResult.files);
18351
+ try {
18352
+ writeGeneratedFiles(projectDir, generateResult.files);
18353
+ } catch (err) {
18354
+ output.error("UNSAFE_GENERATED_PATH", err instanceof Error ? err.message : "Unsafe generated file path", "The AI response contained a file path outside the project directory and was rejected.");
18355
+ return;
18356
+ }
18188
18357
  output.info(`Files written to ${projectDir}`);
18189
18358
  if (opts.deploy !== false) {
18190
18359
  output.info("Run `dura deploy --project " + projectId + " --dir " + projectDir + "` to deploy");
@@ -19249,7 +19418,7 @@ var init_heal = __esm(() => {
19249
19418
  });
19250
19419
 
19251
19420
  // src/lib/skill-installer.ts
19252
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "node:fs";
19421
+ import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "node:fs";
19253
19422
  import { join as join13, dirname as dirname3 } from "node:path";
19254
19423
  import { homedir as homedir2 } from "node:os";
19255
19424
  import { fileURLToPath } from "node:url";
@@ -19265,14 +19434,14 @@ function getLocalSkillsDir(projectDir) {
19265
19434
  }
19266
19435
  function installSkills(targetDir) {
19267
19436
  const sourceDir = getSkillSourceDir();
19268
- if (!existsSync12(targetDir)) {
19437
+ if (!existsSync13(targetDir)) {
19269
19438
  mkdirSync7(targetDir, { recursive: true });
19270
19439
  }
19271
19440
  const installedFiles = [];
19272
19441
  for (const file of SKILL_FILES) {
19273
19442
  const sourcePath = join13(sourceDir, file);
19274
19443
  const targetPath = join13(targetDir, file);
19275
- const content = readFileSync11(sourcePath, "utf-8");
19444
+ const content = readFileSync12(sourcePath, "utf-8");
19276
19445
  writeFileSync10(targetPath, content, "utf-8");
19277
19446
  installedFiles.push(targetPath);
19278
19447
  }
@@ -19297,7 +19466,7 @@ var exports_init = {};
19297
19466
  __export(exports_init, {
19298
19467
  registerInitCommand: () => registerInitCommand
19299
19468
  });
19300
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "node:fs";
19469
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "node:fs";
19301
19470
  import { join as join14, resolve as resolve6, basename } from "node:path";
19302
19471
  function registerInitCommand(program2) {
19303
19472
  program2.command("init").description("Initialize a dura project in the current directory and install AI agent skills").option("--dir <path>", "Project directory (defaults to current dir)").option("--name <name>", "Project name (defaults to directory name)").option("--global", "Install AI agent skills globally (~/.claude/skills/dura/)").option("--local", "Install AI agent skills locally (.claude/skills/dura/)").option("--skip-skills", "Skip AI agent skill installation").action(async (opts, cmd) => {
@@ -19311,24 +19480,24 @@ function registerInitCommand(program2) {
19311
19480
  mkdirSync8(join14(projectDir, "routes"), { recursive: true });
19312
19481
  mkdirSync8(join14(projectDir, "jobs"), { recursive: true });
19313
19482
  const manifestPath = join14(projectDir, "dura.json");
19314
- const alreadyInitialized = existsSync13(manifestPath);
19483
+ const alreadyInitialized = existsSync14(manifestPath);
19315
19484
  if (!alreadyInitialized) {
19316
19485
  writeFileSync11(manifestPath, duraJsonTemplate(projectName));
19317
19486
  } else {
19318
19487
  output.info(`dura.json already exists in ${projectDir} — leaving it in place.`);
19319
19488
  }
19320
19489
  const helloPath = join14(projectDir, "routes", "hello.ts");
19321
- if (existsSync13(helloPath)) {
19490
+ if (existsSync14(helloPath)) {
19322
19491
  output.warn("routes/hello.ts already exists — skipping");
19323
19492
  } else {
19324
19493
  writeFileSync11(helloPath, HELLO_TEMPLATE);
19325
19494
  }
19326
19495
  const gitignorePath = join14(projectDir, ".gitignore");
19327
- if (!existsSync13(gitignorePath)) {
19496
+ if (!existsSync14(gitignorePath)) {
19328
19497
  writeFileSync11(gitignorePath, GITIGNORE_CONTENT2);
19329
19498
  }
19330
19499
  const pkgPath = join14(projectDir, "package.json");
19331
- if (!existsSync13(pkgPath)) {
19500
+ if (!existsSync14(pkgPath)) {
19332
19501
  writeFileSync11(pkgPath, packageJsonTemplate(projectName));
19333
19502
  }
19334
19503
  if (alreadyInitialized) {
@@ -19548,29 +19717,36 @@ async function registerAllCommands(program2) {
19548
19717
  const { registerHealCommand: registerHealCommand2 } = await Promise.resolve().then(() => (init_heal(), exports_heal));
19549
19718
  const { registerInitCommand: registerInitCommand2 } = await Promise.resolve().then(() => (init_init(), exports_init));
19550
19719
  const { registerProjectsCommand: registerProjectsCommand2 } = await Promise.resolve().then(() => (init_projects2(), exports_projects));
19720
+ program2.commandsGroup("Auth & setup:");
19551
19721
  registerLoginCommand2(program2);
19552
19722
  registerLogoutCommand2(program2);
19553
19723
  registerConfigCommand2(program2);
19724
+ program2.commandsGroup("Projects:");
19554
19725
  registerNewCommand2(program2);
19555
19726
  registerInitCommand2(program2);
19556
19727
  registerCreateCommand2(program2);
19557
19728
  registerAddCommand2(program2);
19558
19729
  registerProjectsCommand2(program2);
19730
+ program2.commandsGroup("Templates:");
19559
19731
  registerTemplateCommand2(program2);
19732
+ program2.commandsGroup("Config:");
19560
19733
  registerSecretsCommand2(program2);
19561
19734
  registerDomainsCommand2(program2);
19562
19735
  registerEndpointKeysCommand2(program2);
19563
19736
  registerEnvCommand2(program2);
19737
+ program2.commandsGroup("Deploy lifecycle:");
19564
19738
  registerDeployCommand2(program2);
19565
19739
  registerRollbackCommand2(program2);
19566
19740
  registerDeploymentsCommand2(program2);
19567
19741
  registerPromoteCommand2(program2);
19568
19742
  registerCanaryCommand2(program2);
19743
+ program2.commandsGroup("Run & develop:");
19569
19744
  registerDevCommand2(program2);
19570
19745
  registerTestCommand2(program2);
19571
19746
  registerRunCommand2(program2);
19572
19747
  registerScheduleCommand2(program2);
19573
19748
  registerEventsCommand2(program2);
19749
+ program2.commandsGroup("Observe & debug:");
19574
19750
  registerStatusCommand2(program2);
19575
19751
  registerLogsCommand2(program2);
19576
19752
  registerUsageCommand2(program2);
@@ -19578,6 +19754,7 @@ async function registerAllCommands(program2) {
19578
19754
  registerReplayCommand2(program2);
19579
19755
  registerReplaysCommand2(program2);
19580
19756
  registerDiagnoseCommand2(program2);
19757
+ program2.commandsGroup("Workflows & ops:");
19581
19758
  registerWorkflowsCommand2(program2);
19582
19759
  registerWorkflowCommand2(program2);
19583
19760
  registerApprovalsCommand2(program2);
@@ -19585,14 +19762,16 @@ async function registerAllCommands(program2) {
19585
19762
  registerRejectCommand2(program2);
19586
19763
  registerHealCommand2(program2);
19587
19764
  registerReportsCommand2(program2);
19765
+ program2.commandsGroup("Integrations & storage:");
19588
19766
  registerWebhookCommand2(program2);
19589
19767
  registerMarketplaceCommand2(program2);
19590
19768
  registerKvCommand2(program2);
19769
+ program2.commandsGroup("Export & introspection:");
19591
19770
  registerExportCommand2(program2);
19592
19771
  registerOpenApiCommand2(program2);
19593
19772
  return program2;
19594
19773
  }
19595
- var CLI_VERSION = "0.3.3";
19774
+ var CLI_VERSION = "0.4.0";
19596
19775
  var init_src3 = __esm(() => {
19597
19776
  init_esm();
19598
19777
  if (import.meta.url === `file://${realpathSync(process.argv[1] ?? "").replace(/\\/g, "/")}`) {
@@ -19609,4 +19788,4 @@ export {
19609
19788
  CLI_VERSION
19610
19789
  };
19611
19790
 
19612
- //# debugId=865D5FDBF88E91E164756E2164756E21
19791
+ //# debugId=BACD50D24CC529CE64756E2164756E21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dura-run/cli",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "CLI for the dura.run managed automation platform",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -10,13 +10,12 @@ Handlers run in a V8 isolate on dura.run. **No Node built-ins, no filesystem, no
10
10
 
11
11
  - Standard JavaScript globals (`Date`, `Math`, `JSON`, `Promise`, `Uint8Array`, etc.)
12
12
  - `crypto.randomUUID()` and `crypto.getRandomValues(typedArray)` (WebCrypto-spec random)
13
+ - `crypto.subtle.*` — full WebCrypto: `digest`, `sign`/`verify`, `encrypt`/`decrypt`, `generateKey`, `importKey`/`exportKey`, `deriveKey`/`deriveBits`, `wrapKey`/`unwrapKey`
13
14
  - `TextEncoder` and `TextDecoder` (UTF-8 only)
14
15
  - The fetch API (via `dura.fetch`)
15
16
  - The `dura.*` SDK surface: `fetch`, `log`, `env`, `kv`, `trigger`, `triggerAndWait`
16
17
 
17
- **What is NOT available yet:**
18
-
19
- - `crypto.subtle.*` (hashing, signing, key derivation). For these, call a trusted external service via `dura.fetch`.
18
+ **Note on `crypto.subtle`:** it is bridged to the host runtime, so each call blocks the isolate while it runs. `await`ing a few signs/hashes per request is fine, but `Promise.all([...subtle calls])` executes serially, not concurrently — don't rely on parallelism for throughput, and avoid bulk hashing in tight loops.
20
19
 
21
20
  **Use these replacements for common Node-isms:**
22
21
 
@@ -25,7 +24,7 @@ Handlers run in a V8 isolate on dura.run. **No Node built-ins, no filesystem, no
25
24
  | `node:fs` | `dura.kv` or the artifact APIs |
26
25
  | `node:crypto` for IDs | `crypto.randomUUID()` |
27
26
  | `node:crypto` for random bytes | `crypto.getRandomValues(new Uint8Array(n))` |
28
- | `node:crypto` for hashing/signing | Not yet supported call an external service via `dura.fetch` |
27
+ | `node:crypto` for hashing/signing | `crypto.subtle.digest` / `crypto.subtle.sign` / `crypto.subtle.verify` (WebCrypto) |
29
28
  | `Buffer` | `Uint8Array` with `TextEncoder` / `TextDecoder` |
30
29
  | `node:child_process` | Not supported — there is no process boundary to use |
31
30
  | `node:http` / `https` | `dura.fetch` |