@coresource/hz 0.20.0 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/hz.mjs +709 -151
  2. package/package.json +2 -2
package/dist/hz.mjs CHANGED
@@ -867,10 +867,9 @@ function registerMissionListCommand(mission, context, dependencies = {}) {
867
867
  }
868
868
 
869
869
  // src/commands/mission-lifecycle.ts
870
- import { readdir as readdir2, readFile as readFile3, stat } from "fs/promises";
871
- import path3 from "path";
870
+ import { confirm as defaultConfirmPrompt } from "@inquirer/prompts";
872
871
  import { CommanderError } from "commander";
873
- import pc5 from "picocolors";
872
+ import pc6 from "picocolors";
874
873
 
875
874
  // src/monitor.ts
876
875
  import { setTimeout as delay2 } from "timers/promises";
@@ -2705,13 +2704,99 @@ async function monitorMission(options) {
2705
2704
  }
2706
2705
  }
2707
2706
 
2708
- // src/commands/mission-lifecycle.ts
2707
+ // src/commands/mission-id.ts
2708
+ import { readdir as readdir2, readFile as readFile3, stat } from "fs/promises";
2709
+ import path3 from "path";
2710
+ import pc5 from "picocolors";
2709
2711
  function asNonEmptyString5(value) {
2710
2712
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
2711
2713
  }
2714
+ function resolveMissionCommandHomeDir(context) {
2715
+ return resolveHomeDir({
2716
+ env: context.env,
2717
+ homeDir: context.homeDir
2718
+ });
2719
+ }
2720
+ async function findMostRecentMissionId(homeDir) {
2721
+ const launchesDir = path3.join(homeDir, ".hz", "cloud-launches");
2722
+ let entries;
2723
+ try {
2724
+ entries = await readdir2(launchesDir, { withFileTypes: true });
2725
+ } catch (error) {
2726
+ if (error.code === "ENOENT") {
2727
+ return null;
2728
+ }
2729
+ throw error;
2730
+ }
2731
+ const candidates = await Promise.all(
2732
+ entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
2733
+ const missionPath = path3.join(launchesDir, entry.name, "mission.json");
2734
+ try {
2735
+ const [raw, missionStats] = await Promise.all([
2736
+ readFile3(missionPath, "utf8"),
2737
+ stat(missionPath)
2738
+ ]);
2739
+ const parsed = JSON.parse(raw);
2740
+ const missionId = asNonEmptyString5(parsed.missionId);
2741
+ if (!missionId) {
2742
+ return null;
2743
+ }
2744
+ return {
2745
+ missionId,
2746
+ updatedAtMs: missionStats.mtimeMs
2747
+ };
2748
+ } catch (error) {
2749
+ if (error instanceof SyntaxError || error.code === "ENOENT") {
2750
+ return null;
2751
+ }
2752
+ throw error;
2753
+ }
2754
+ })
2755
+ );
2756
+ const latest = candidates.filter((candidate) => candidate !== null).sort(
2757
+ (left, right) => right.updatedAtMs - left.updatedAtMs || right.missionId.localeCompare(left.missionId)
2758
+ )[0];
2759
+ return latest?.missionId ?? null;
2760
+ }
2761
+ async function resolveMissionId(missionId, context, command, homeDir) {
2762
+ const explicitMissionId = asNonEmptyString5(missionId);
2763
+ if (explicitMissionId) {
2764
+ return explicitMissionId;
2765
+ }
2766
+ const detectedMissionId = await findMostRecentMissionId(homeDir);
2767
+ if (detectedMissionId) {
2768
+ writeLine(
2769
+ context.stdout,
2770
+ `Using the most recent local mission: ${pc5.cyan(detectedMissionId)}`
2771
+ );
2772
+ return detectedMissionId;
2773
+ }
2774
+ writeLine(
2775
+ context.stderr,
2776
+ "Mission ID is required when no recent local mission state is available."
2777
+ );
2778
+ command.help({ error: true });
2779
+ }
2780
+
2781
+ // src/commands/mission-lifecycle.ts
2782
+ function isRecord5(value) {
2783
+ return value !== null && !Array.isArray(value) && typeof value === "object";
2784
+ }
2785
+ function asNonEmptyString6(value) {
2786
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
2787
+ }
2788
+ function asFiniteNumber4(value) {
2789
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
2790
+ }
2791
+ async function defaultPromptConfirm(options) {
2792
+ return defaultConfirmPrompt({
2793
+ default: options.default,
2794
+ message: options.message
2795
+ });
2796
+ }
2712
2797
  function writeSection(stdout, title) {
2713
2798
  writeLine(stdout);
2714
- writeLine(stdout, pc5.bold(title));
2799
+ writeLine(stdout, pc6.bold(title));
2715
2800
  }
2716
2801
  function summarizeAssertions(assertions) {
2717
2802
  return assertions.reduce(
@@ -2800,72 +2885,6 @@ async function createMissionClientContext(context, command, dependencies, homeDi
2800
2885
  homeDir
2801
2886
  };
2802
2887
  }
2803
- function resolveLifecycleHomeDir(context) {
2804
- return resolveHomeDir({
2805
- env: context.env,
2806
- homeDir: context.homeDir
2807
- });
2808
- }
2809
- async function findMostRecentMissionId(homeDir) {
2810
- const launchesDir = path3.join(homeDir, ".hz", "cloud-launches");
2811
- let entries;
2812
- try {
2813
- entries = await readdir2(launchesDir, { withFileTypes: true });
2814
- } catch (error) {
2815
- if (error.code === "ENOENT") {
2816
- return null;
2817
- }
2818
- throw error;
2819
- }
2820
- const candidates = await Promise.all(
2821
- entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
2822
- const missionPath = path3.join(launchesDir, entry.name, "mission.json");
2823
- try {
2824
- const [raw, missionStats] = await Promise.all([
2825
- readFile3(missionPath, "utf8"),
2826
- stat(missionPath)
2827
- ]);
2828
- const parsed = JSON.parse(raw);
2829
- const missionId = asNonEmptyString5(parsed.missionId);
2830
- if (!missionId) {
2831
- return null;
2832
- }
2833
- return {
2834
- missionId,
2835
- updatedAtMs: missionStats.mtimeMs
2836
- };
2837
- } catch (error) {
2838
- if (error instanceof SyntaxError || error.code === "ENOENT") {
2839
- return null;
2840
- }
2841
- throw error;
2842
- }
2843
- })
2844
- );
2845
- const latest = candidates.filter((candidate) => candidate !== null).sort(
2846
- (left, right) => right.updatedAtMs - left.updatedAtMs || right.missionId.localeCompare(left.missionId)
2847
- )[0];
2848
- return latest?.missionId ?? null;
2849
- }
2850
- async function resolveMissionId(missionId, context, command, homeDir) {
2851
- const explicitMissionId = asNonEmptyString5(missionId);
2852
- if (explicitMissionId) {
2853
- return explicitMissionId;
2854
- }
2855
- const detectedMissionId = await findMostRecentMissionId(homeDir);
2856
- if (detectedMissionId) {
2857
- writeLine(
2858
- context.stdout,
2859
- `Using the most recent local mission: ${pc5.cyan(detectedMissionId)}`
2860
- );
2861
- return detectedMissionId;
2862
- }
2863
- writeLine(
2864
- context.stderr,
2865
- "Mission ID is required when no recent local mission state is available."
2866
- );
2867
- command.help({ error: true });
2868
- }
2869
2888
  async function fetchMissionStateAfterConflict(apiClient, missionId, error) {
2870
2889
  try {
2871
2890
  return await fetchMissionStateRecord(apiClient, missionId);
@@ -2880,8 +2899,147 @@ async function fetchMissionSnapshotAfterConflict(apiClient, missionId, error) {
2880
2899
  throw error;
2881
2900
  }
2882
2901
  }
2902
+ function normalizeMissionDeleteStep(value) {
2903
+ if (!isRecord5(value)) {
2904
+ return {
2905
+ detail: null,
2906
+ launchId: null,
2907
+ objectsDeleted: null,
2908
+ rowsDeleted: null,
2909
+ status: "unknown",
2910
+ tables: {}
2911
+ };
2912
+ }
2913
+ const rawTables = isRecord5(value.tables) ? value.tables : {};
2914
+ const tables = {};
2915
+ for (const [tableName, count] of Object.entries(rawTables)) {
2916
+ const normalizedCount = asFiniteNumber4(count);
2917
+ if (normalizedCount !== null) {
2918
+ tables[tableName] = normalizedCount;
2919
+ }
2920
+ }
2921
+ return {
2922
+ detail: asNonEmptyString6(value.detail),
2923
+ launchId: asNonEmptyString6(value.launch_id),
2924
+ objectsDeleted: asFiniteNumber4(value.objects_deleted),
2925
+ rowsDeleted: asFiniteNumber4(value.rows_deleted),
2926
+ status: asNonEmptyString6(value.status) ?? "unknown",
2927
+ tables
2928
+ };
2929
+ }
2930
+ function normalizeMissionDeleteResponse(value) {
2931
+ if (!isRecord5(value)) {
2932
+ throw new CliError("Mission delete response was not an object.");
2933
+ }
2934
+ const missionId = asNonEmptyString6(value.mission_id);
2935
+ if (!missionId) {
2936
+ throw new CliError("Mission delete response is missing a mission_id.");
2937
+ }
2938
+ const rawSteps = isRecord5(value.steps) ? value.steps : {};
2939
+ const steps = {};
2940
+ for (const [stepName, stepValue] of Object.entries(rawSteps)) {
2941
+ steps[stepName] = normalizeMissionDeleteStep(stepValue);
2942
+ }
2943
+ return {
2944
+ deleted: value.deleted === true,
2945
+ missionId,
2946
+ steps
2947
+ };
2948
+ }
2949
+ function formatMissionDeleteStatus(status) {
2950
+ switch (status) {
2951
+ case "success":
2952
+ return pc6.green(status);
2953
+ case "failed":
2954
+ return pc6.red(status);
2955
+ case "skipped":
2956
+ return pc6.yellow(status);
2957
+ default:
2958
+ return status;
2959
+ }
2960
+ }
2961
+ function formatMissionDeleteStepSummary(step) {
2962
+ const parts = [];
2963
+ if (step.detail) {
2964
+ parts.push(step.detail);
2965
+ }
2966
+ if (step.rowsDeleted !== null) {
2967
+ parts.push(`rows deleted: ${step.rowsDeleted}`);
2968
+ }
2969
+ if (step.objectsDeleted !== null) {
2970
+ parts.push(`objects deleted: ${step.objectsDeleted}`);
2971
+ }
2972
+ const tableEntries = Object.entries(step.tables).sort(
2973
+ ([left], [right]) => left.localeCompare(right)
2974
+ );
2975
+ if (tableEntries.length > 0) {
2976
+ parts.push(
2977
+ `tables: ${tableEntries.map(([tableName, count]) => `${tableName}=${count}`).join(", ")}`
2978
+ );
2979
+ }
2980
+ if (step.launchId) {
2981
+ parts.push(`launch id: ${step.launchId}`);
2982
+ }
2983
+ return parts.join(" \xB7 ") || "\u2014";
2984
+ }
2985
+ function sortMissionDeleteSteps(steps) {
2986
+ const orderedSteps = [
2987
+ "cancel",
2988
+ "revoke_key",
2989
+ "fleet_db",
2990
+ "missions_db",
2991
+ "r2_objects",
2992
+ "kv_entries",
2993
+ "planner_session",
2994
+ "orphaned_launch",
2995
+ "do_storage"
2996
+ ];
2997
+ const seen = /* @__PURE__ */ new Set();
2998
+ const result = [];
2999
+ for (const stepName of orderedSteps) {
3000
+ const step = steps[stepName];
3001
+ if (!step) {
3002
+ continue;
3003
+ }
3004
+ seen.add(stepName);
3005
+ result.push([stepName, step]);
3006
+ }
3007
+ for (const [stepName, step] of Object.entries(steps).sort(
3008
+ ([left], [right]) => left.localeCompare(right)
3009
+ )) {
3010
+ if (seen.has(stepName)) {
3011
+ continue;
3012
+ }
3013
+ result.push([stepName, step]);
3014
+ }
3015
+ return result;
3016
+ }
3017
+ function renderMissionDeleteResponse(stdout, response) {
3018
+ writeLine(stdout, `Mission ${response.missionId} deleted successfully.`);
3019
+ const failedSteps = Object.entries(response.steps).filter(
3020
+ ([, step]) => step.status === "failed"
3021
+ );
3022
+ if (failedSteps.length > 0) {
3023
+ writeLine(
3024
+ stdout,
3025
+ pc6.yellow(
3026
+ `Warning: Mission cleanup completed with cleanup warnings (${failedSteps.length} failed step${failedSteps.length === 1 ? "" : "s"}).`
3027
+ )
3028
+ );
3029
+ }
3030
+ writeSection(stdout, "Cleanup steps");
3031
+ renderTable(
3032
+ stdout,
3033
+ ["Step", "Status", "Result"],
3034
+ sortMissionDeleteSteps(response.steps).map(([stepName, step]) => [
3035
+ plainCell(stepName),
3036
+ coloredCell(formatMissionDeleteStatus(step.status), step.status),
3037
+ plainCell(formatMissionDeleteStepSummary(step))
3038
+ ])
3039
+ );
3040
+ }
2883
3041
  async function runLifecycleMutation(action, missionIdValue, command, context, dependencies) {
2884
- const homeDir = resolveLifecycleHomeDir(context);
3042
+ const homeDir = resolveMissionCommandHomeDir(context);
2885
3043
  const missionId = await resolveMissionId(missionIdValue, context, command, homeDir);
2886
3044
  const { apiClient } = await createMissionClientContext(
2887
3045
  context,
@@ -2895,17 +3053,17 @@ async function runLifecycleMutation(action, missionIdValue, command, context, de
2895
3053
  method: "POST",
2896
3054
  path: `/missions/${missionId}/mission/${action}`
2897
3055
  });
2898
- const nextState = asNonEmptyString5(response.state) ?? (action === "pause" ? "paused" : "cancelled");
3056
+ const nextState = asNonEmptyString6(response.state) ?? (action === "pause" ? "paused" : "cancelled");
2899
3057
  if (action === "pause") {
2900
3058
  writeLine(
2901
3059
  context.stdout,
2902
- `Mission ${pc5.cyan(missionId)} paused. Current state: ${pc5.bold(nextState)}.`
3060
+ `Mission ${pc6.cyan(missionId)} paused. Current state: ${pc6.bold(nextState)}.`
2903
3061
  );
2904
3062
  return;
2905
3063
  }
2906
3064
  writeLine(
2907
3065
  context.stdout,
2908
- `Mission ${pc5.cyan(missionId)} cancelled. This action is irreversible. Current state: ${pc5.bold(nextState)}.`
3066
+ `Mission ${pc6.cyan(missionId)} cancelled. This action is irreversible. Current state: ${pc6.bold(nextState)}.`
2909
3067
  );
2910
3068
  } catch (error) {
2911
3069
  if (!(error instanceof ApiError) || error.status !== 409) {
@@ -2919,11 +3077,51 @@ async function runLifecycleMutation(action, missionIdValue, command, context, de
2919
3077
  const verb = action === "pause" ? "paused" : "cancelled";
2920
3078
  writeLine(
2921
3079
  context.stdout,
2922
- `Mission ${pc5.cyan(missionId)} cannot be ${verb} because it is currently ${pc5.bold(missionState.state)}.`
3080
+ `Mission ${pc6.cyan(missionId)} cannot be ${verb} because it is currently ${pc6.bold(missionState.state)}.`
2923
3081
  );
2924
3082
  throw new CommanderError(1, `mission-${action}`, "");
2925
3083
  }
2926
3084
  }
3085
+ function buildDeleteConfirmationMessage(missionId, forceDelete) {
3086
+ const lines = [
3087
+ `Are you sure you want to delete mission ${missionId}? This cannot be undone.`
3088
+ ];
3089
+ if (forceDelete) {
3090
+ lines.push("This will cancel the running mission and delete all resources.");
3091
+ }
3092
+ return lines.join("\n");
3093
+ }
3094
+ async function runMissionDelete(missionIdValue, options, command, context, dependencies) {
3095
+ const homeDir = resolveMissionCommandHomeDir(context);
3096
+ const missionId = await resolveMissionId(missionIdValue, context, command, homeDir);
3097
+ if (options.yes !== true) {
3098
+ const confirmed = await (dependencies.promptConfirm ?? defaultPromptConfirm)({
3099
+ default: false,
3100
+ message: buildDeleteConfirmationMessage(missionId, options.force === true)
3101
+ });
3102
+ if (!confirmed) {
3103
+ writeLine(context.stdout, pc6.yellow("Mission deletion cancelled."));
3104
+ return;
3105
+ }
3106
+ }
3107
+ const { apiClient } = await createMissionClientContext(
3108
+ context,
3109
+ command,
3110
+ dependencies,
3111
+ homeDir
3112
+ );
3113
+ const query = options.force === true ? "?force=true" : "";
3114
+ const response = normalizeMissionDeleteResponse(
3115
+ await apiClient.request({
3116
+ method: "DELETE",
3117
+ path: `/missions/${missionId}${query}`
3118
+ })
3119
+ );
3120
+ if (!response.deleted) {
3121
+ throw new CliError(`Mission ${missionId} was not deleted.`, 1);
3122
+ }
3123
+ renderMissionDeleteResponse(context.stdout, response);
3124
+ }
2927
3125
  function resolveResumeExitCode(state) {
2928
3126
  switch (state) {
2929
3127
  case "cancelled":
@@ -2948,7 +3146,7 @@ function resolveWatchExitCode(state) {
2948
3146
  }
2949
3147
  }
2950
3148
  async function runMissionResume(missionIdValue, command, context, dependencies) {
2951
- const homeDir = resolveLifecycleHomeDir(context);
3149
+ const homeDir = resolveMissionCommandHomeDir(context);
2952
3150
  const missionId = await resolveMissionId(missionIdValue, context, command, homeDir);
2953
3151
  const { apiClient, authConfig } = await createMissionClientContext(
2954
3152
  context,
@@ -2964,10 +3162,10 @@ async function runMissionResume(missionIdValue, command, context, dependencies)
2964
3162
  method: "POST",
2965
3163
  path: `/missions/${missionId}/mission/resume`
2966
3164
  });
2967
- const nextState = asNonEmptyString5(response.state) ?? "orchestrator_turn";
3165
+ const nextState = asNonEmptyString6(response.state) ?? "orchestrator_turn";
2968
3166
  writeLine(
2969
3167
  context.stdout,
2970
- `Mission ${pc5.cyan(missionId)} resumed. Current state: ${pc5.bold(nextState)}.`
3168
+ `Mission ${pc6.cyan(missionId)} resumed. Current state: ${pc6.bold(nextState)}.`
2971
3169
  );
2972
3170
  } catch (error) {
2973
3171
  if (!(error instanceof ApiError) || error.status !== 409) {
@@ -2980,14 +3178,14 @@ async function runMissionResume(missionIdValue, command, context, dependencies)
2980
3178
  );
2981
3179
  writeLine(
2982
3180
  context.stdout,
2983
- `Mission ${pc5.cyan(missionId)} is currently ${pc5.bold(snapshot.state.state)}. Reconnecting to live updates if available.`
3181
+ `Mission ${pc6.cyan(missionId)} is currently ${pc6.bold(snapshot.state.state)}. Reconnecting to live updates if available.`
2984
3182
  );
2985
3183
  }
2986
3184
  snapshot = await fetchMissionSnapshot(apiClient, missionId);
2987
3185
  } else if (snapshot.state.state !== "cancelled" && snapshot.state.state !== "completed") {
2988
3186
  writeLine(
2989
3187
  context.stdout,
2990
- `Reconnecting to mission ${pc5.cyan(missionId)} from state ${pc5.bold(snapshot.state.state)}.`
3188
+ `Reconnecting to mission ${pc6.cyan(missionId)} from state ${pc6.bold(snapshot.state.state)}.`
2991
3189
  );
2992
3190
  }
2993
3191
  renderMissionSnapshot(context.stdout, snapshot);
@@ -3019,7 +3217,7 @@ async function runMissionResume(missionIdValue, command, context, dependencies)
3019
3217
  }
3020
3218
  }
3021
3219
  async function runMissionWatch(missionIdValue, command, context, dependencies) {
3022
- const homeDir = resolveLifecycleHomeDir(context);
3220
+ const homeDir = resolveMissionCommandHomeDir(context);
3023
3221
  const missionId = await resolveMissionId(missionIdValue, context, command, homeDir);
3024
3222
  const { apiClient, authConfig } = await createMissionClientContext(
3025
3223
  context,
@@ -3030,7 +3228,7 @@ async function runMissionWatch(missionIdValue, command, context, dependencies) {
3030
3228
  const snapshot = await fetchMissionSnapshot(apiClient, missionId);
3031
3229
  writeLine(
3032
3230
  context.stdout,
3033
- `Watching mission ${pc5.cyan(missionId)} from state ${pc5.bold(snapshot.state.state)}.`
3231
+ `Watching mission ${pc6.cyan(missionId)} from state ${pc6.bold(snapshot.state.state)}.`
3034
3232
  );
3035
3233
  renderMissionSnapshot(context.stdout, snapshot);
3036
3234
  const watchExitCode = resolveWatchExitCode(snapshot.state.state);
@@ -3085,6 +3283,365 @@ function registerMissionLifecycleCommands(mission, context, dependencies = {}) {
3085
3283
  dependencies
3086
3284
  );
3087
3285
  });
3286
+ mission.command("delete").description("Delete a mission and clean up its resources").argument("[missionId]").option("--force", "Force-cancel active missions before deletion").option("--yes", "Skip the confirmation prompt").action(async (missionId, options, command) => {
3287
+ await runMissionDelete(
3288
+ missionId,
3289
+ options,
3290
+ command,
3291
+ context,
3292
+ dependencies
3293
+ );
3294
+ });
3295
+ }
3296
+
3297
+ // src/commands/mission-resources.ts
3298
+ import pc7 from "picocolors";
3299
+ function isRecord6(value) {
3300
+ return value !== null && !Array.isArray(value) && typeof value === "object";
3301
+ }
3302
+ function asBoolean(value) {
3303
+ return value === true;
3304
+ }
3305
+ function asFiniteNumber5(value) {
3306
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
3307
+ }
3308
+ function asNullableNumber3(value) {
3309
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
3310
+ }
3311
+ function asNonEmptyString7(value) {
3312
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
3313
+ }
3314
+ function formatInteger(value) {
3315
+ return new Intl.NumberFormat("en-US", {
3316
+ maximumFractionDigits: 0
3317
+ }).format(value);
3318
+ }
3319
+ function formatDecimal(value, maximumFractionDigits = 2) {
3320
+ return new Intl.NumberFormat("en-US", {
3321
+ maximumFractionDigits,
3322
+ minimumFractionDigits: 0
3323
+ }).format(value);
3324
+ }
3325
+ function formatPercentValue(value) {
3326
+ return `${formatDecimal(value, 2)}%`;
3327
+ }
3328
+ function formatBoolean(value) {
3329
+ return value ? "yes" : "no";
3330
+ }
3331
+ function formatCents(value) {
3332
+ return formatCurrency(value / 100);
3333
+ }
3334
+ function formatBytes(value) {
3335
+ const units = ["B", "KB", "MB", "GB", "TB"];
3336
+ let size = value;
3337
+ let unitIndex = 0;
3338
+ while (size >= 1024 && unitIndex < units.length - 1) {
3339
+ size /= 1024;
3340
+ unitIndex += 1;
3341
+ }
3342
+ const formatted = unitIndex === 0 ? formatInteger(size) : formatDecimal(size, 2);
3343
+ return `${formatted} ${units[unitIndex]}`;
3344
+ }
3345
+ function writeSection2(stdout, title) {
3346
+ writeLine(stdout);
3347
+ writeLine(stdout, pc7.bold(title));
3348
+ }
3349
+ function renderMetricTable(stdout, rows) {
3350
+ renderTable(
3351
+ stdout,
3352
+ ["Metric", "Value"],
3353
+ rows.map(([label, value]) => [plainCell(label), plainCell(value)])
3354
+ );
3355
+ }
3356
+ function renderOptionalTable(stdout, title, headers, rows, emptyMessage) {
3357
+ writeLine(stdout);
3358
+ writeLine(stdout, title);
3359
+ if (rows.length === 0) {
3360
+ writeLine(stdout, emptyMessage);
3361
+ return;
3362
+ }
3363
+ renderTable(
3364
+ stdout,
3365
+ headers,
3366
+ rows.map((row) => row.map((value) => plainCell(value)))
3367
+ );
3368
+ }
3369
+ function normalizeInferenceByModel(value) {
3370
+ if (!Array.isArray(value)) {
3371
+ return [];
3372
+ }
3373
+ return value.flatMap((entry) => {
3374
+ if (!isRecord6(entry)) {
3375
+ return [];
3376
+ }
3377
+ const model = asNonEmptyString7(entry.model);
3378
+ if (!model) {
3379
+ return [];
3380
+ }
3381
+ return [{
3382
+ costUsd: asFiniteNumber5(entry.cost_usd),
3383
+ model,
3384
+ requests: asFiniteNumber5(entry.requests),
3385
+ standardTokens: asFiniteNumber5(entry.standard_tokens),
3386
+ tokens: asFiniteNumber5(entry.tokens)
3387
+ }];
3388
+ });
3389
+ }
3390
+ function normalizeInferenceByFeature(value) {
3391
+ if (!Array.isArray(value)) {
3392
+ return [];
3393
+ }
3394
+ return value.flatMap((entry) => {
3395
+ if (!isRecord6(entry)) {
3396
+ return [];
3397
+ }
3398
+ const featureId = asNonEmptyString7(entry.feature_id);
3399
+ if (!featureId) {
3400
+ return [];
3401
+ }
3402
+ return [{
3403
+ costUsd: asFiniteNumber5(entry.cost_usd),
3404
+ featureId,
3405
+ requests: asFiniteNumber5(entry.requests),
3406
+ standardTokens: asFiniteNumber5(entry.standard_tokens),
3407
+ tokens: asFiniteNumber5(entry.tokens)
3408
+ }];
3409
+ });
3410
+ }
3411
+ function normalizeSandboxByClass(value) {
3412
+ if (!Array.isArray(value)) {
3413
+ return [];
3414
+ }
3415
+ return value.flatMap((entry) => {
3416
+ if (!isRecord6(entry)) {
3417
+ return [];
3418
+ }
3419
+ const instanceClass = asNonEmptyString7(entry.instance_class);
3420
+ if (!instanceClass) {
3421
+ return [];
3422
+ }
3423
+ return [{
3424
+ containers: asFiniteNumber5(entry.containers),
3425
+ costCents: asFiniteNumber5(entry.cost_cents),
3426
+ instanceClass,
3427
+ minutes: asFiniteNumber5(entry.minutes)
3428
+ }];
3429
+ });
3430
+ }
3431
+ function normalizeSandboxContainers(value) {
3432
+ if (!Array.isArray(value)) {
3433
+ return [];
3434
+ }
3435
+ return value.flatMap((entry) => {
3436
+ if (!isRecord6(entry)) {
3437
+ return [];
3438
+ }
3439
+ const containerName = asNonEmptyString7(entry.container_name);
3440
+ if (!containerName) {
3441
+ return [];
3442
+ }
3443
+ return [{
3444
+ containerName,
3445
+ createdAt: asNonEmptyString7(entry.created_at),
3446
+ endedAt: asNonEmptyString7(entry.ended_at),
3447
+ featureId: asNonEmptyString7(entry.feature_id),
3448
+ instanceClass: asNonEmptyString7(entry.instance_class) ?? "unknown",
3449
+ startedAt: asNonEmptyString7(entry.started_at),
3450
+ status: asNonEmptyString7(entry.status) ?? "unknown",
3451
+ workerSessionId: asNonEmptyString7(entry.worker_session_id)
3452
+ }];
3453
+ });
3454
+ }
3455
+ function normalizeStorageD1Rows(value) {
3456
+ if (!isRecord6(value)) {
3457
+ return {};
3458
+ }
3459
+ const normalized = {};
3460
+ for (const [key, count] of Object.entries(value)) {
3461
+ normalized[key] = asFiniteNumber5(count);
3462
+ }
3463
+ return normalized;
3464
+ }
3465
+ function normalizeMissionResourceSummary(value) {
3466
+ if (!isRecord6(value)) {
3467
+ throw new Error("Mission resources response was not an object.");
3468
+ }
3469
+ const missionId = asNonEmptyString7(value.mission_id);
3470
+ if (!missionId) {
3471
+ throw new Error("Mission resources response is missing a mission_id.");
3472
+ }
3473
+ const inference = isRecord6(value.inference) ? value.inference : {};
3474
+ const sandboxCompute = isRecord6(value.sandbox_compute) ? value.sandbox_compute : {};
3475
+ const storage = isRecord6(value.storage) ? value.storage : {};
3476
+ const storageR2 = isRecord6(storage.r2) ? storage.r2 : {};
3477
+ const storageDo = isRecord6(storage.do_storage) ? storage.do_storage : {};
3478
+ const budget = isRecord6(value.budget) ? value.budget : {};
3479
+ return {
3480
+ budget: {
3481
+ exceeded: asBoolean(budget.exceeded),
3482
+ maxInferenceCostUsd: asNullableNumber3(budget.max_inference_cost_usd),
3483
+ maxInferenceTokens: asNullableNumber3(budget.max_inference_tokens),
3484
+ percentUsed: asFiniteNumber5(budget.percent_used),
3485
+ warning: asBoolean(budget.warning)
3486
+ },
3487
+ inference: {
3488
+ byFeature: normalizeInferenceByFeature(inference.by_feature),
3489
+ byModel: normalizeInferenceByModel(inference.by_model),
3490
+ totalCostUsd: asFiniteNumber5(inference.total_cost_usd),
3491
+ totalRequests: asFiniteNumber5(inference.total_requests),
3492
+ totalStandardTokens: asFiniteNumber5(inference.total_standard_tokens),
3493
+ totalTokens: asFiniteNumber5(inference.total_tokens)
3494
+ },
3495
+ missionId,
3496
+ sandboxCompute: {
3497
+ byClass: normalizeSandboxByClass(sandboxCompute.by_class),
3498
+ containers: normalizeSandboxContainers(sandboxCompute.containers),
3499
+ totalContainers: asFiniteNumber5(sandboxCompute.total_containers),
3500
+ totalCostCents: asFiniteNumber5(sandboxCompute.total_cost_cents),
3501
+ totalMinutes: asFiniteNumber5(sandboxCompute.total_minutes)
3502
+ },
3503
+ snapshotAt: asNonEmptyString7(value.snapshot_at),
3504
+ storage: {
3505
+ d1Rows: normalizeStorageD1Rows(storage.d1_rows),
3506
+ doStorage: {
3507
+ progressEventCount: asFiniteNumber5(storageDo.progress_event_count)
3508
+ },
3509
+ r2: {
3510
+ objectCount: asFiniteNumber5(storageR2.object_count),
3511
+ totalBytes: asFiniteNumber5(storageR2.total_bytes)
3512
+ }
3513
+ },
3514
+ tenantId: asNonEmptyString7(value.tenant_id)
3515
+ };
3516
+ }
3517
+ function renderMissionResourceSummary(stdout, summary) {
3518
+ writeLine(stdout, `Mission ${summary.missionId}`);
3519
+ writeLine(stdout, `Tenant: ${summary.tenantId ?? "\u2014"}`);
3520
+ writeLine(stdout, `Snapshot: ${formatDateTime(summary.snapshotAt)}`);
3521
+ writeSection2(stdout, "Inference");
3522
+ renderMetricTable(stdout, [
3523
+ ["Total tokens", formatInteger(summary.inference.totalTokens)],
3524
+ ["Standard tokens", formatInteger(summary.inference.totalStandardTokens)],
3525
+ ["Cost", formatCurrency(summary.inference.totalCostUsd)],
3526
+ ["Requests", formatInteger(summary.inference.totalRequests)]
3527
+ ]);
3528
+ renderOptionalTable(
3529
+ stdout,
3530
+ "By model",
3531
+ ["Model", "Tokens", "Standard tokens", "Cost", "Requests"],
3532
+ summary.inference.byModel.map((entry) => [
3533
+ entry.model,
3534
+ formatInteger(entry.tokens),
3535
+ formatInteger(entry.standardTokens),
3536
+ formatCurrency(entry.costUsd),
3537
+ formatInteger(entry.requests)
3538
+ ]),
3539
+ "No per-model inference usage recorded."
3540
+ );
3541
+ renderOptionalTable(
3542
+ stdout,
3543
+ "By feature",
3544
+ ["Feature", "Tokens", "Standard tokens", "Cost", "Requests"],
3545
+ summary.inference.byFeature.map((entry) => [
3546
+ entry.featureId,
3547
+ formatInteger(entry.tokens),
3548
+ formatInteger(entry.standardTokens),
3549
+ formatCurrency(entry.costUsd),
3550
+ formatInteger(entry.requests)
3551
+ ]),
3552
+ "No per-feature inference usage recorded."
3553
+ );
3554
+ writeSection2(stdout, "Sandbox compute");
3555
+ renderMetricTable(stdout, [
3556
+ ["Total containers", formatInteger(summary.sandboxCompute.totalContainers)],
3557
+ ["Total minutes", formatInteger(summary.sandboxCompute.totalMinutes)],
3558
+ ["Cost", formatCents(summary.sandboxCompute.totalCostCents)]
3559
+ ]);
3560
+ renderOptionalTable(
3561
+ stdout,
3562
+ "Per class",
3563
+ ["Instance class", "Containers", "Minutes", "Cost"],
3564
+ summary.sandboxCompute.byClass.map((entry) => [
3565
+ entry.instanceClass,
3566
+ formatInteger(entry.containers),
3567
+ formatInteger(entry.minutes),
3568
+ formatCents(entry.costCents)
3569
+ ]),
3570
+ "No sandbox class breakdown recorded."
3571
+ );
3572
+ renderOptionalTable(
3573
+ stdout,
3574
+ "Containers",
3575
+ [
3576
+ "Container",
3577
+ "Feature",
3578
+ "Worker",
3579
+ "Class",
3580
+ "Status",
3581
+ "Created",
3582
+ "Started",
3583
+ "Ended"
3584
+ ],
3585
+ summary.sandboxCompute.containers.map((entry) => [
3586
+ entry.containerName,
3587
+ entry.featureId ?? "\u2014",
3588
+ entry.workerSessionId ?? "\u2014",
3589
+ entry.instanceClass,
3590
+ entry.status,
3591
+ formatDateTime(entry.createdAt),
3592
+ formatDateTime(entry.startedAt),
3593
+ formatDateTime(entry.endedAt)
3594
+ ]),
3595
+ "No sandbox containers recorded."
3596
+ );
3597
+ writeSection2(stdout, "Storage");
3598
+ renderMetricTable(stdout, [
3599
+ ["R2 objects", formatInteger(summary.storage.r2.objectCount)],
3600
+ ["R2 bytes", formatBytes(summary.storage.r2.totalBytes)],
3601
+ [
3602
+ "Progress events",
3603
+ formatInteger(summary.storage.doStorage.progressEventCount)
3604
+ ]
3605
+ ]);
3606
+ renderOptionalTable(
3607
+ stdout,
3608
+ "D1 rows",
3609
+ ["Table", "Rows"],
3610
+ Object.entries(summary.storage.d1Rows).sort(([left], [right]) => left.localeCompare(right)).map(([tableName, count]) => [tableName, formatInteger(count)]),
3611
+ "No mission D1 rows recorded."
3612
+ );
3613
+ writeSection2(stdout, "Budget");
3614
+ renderMetricTable(stdout, [
3615
+ [
3616
+ "Max inference cost",
3617
+ summary.budget.maxInferenceCostUsd === null ? "\u2014" : formatCurrency(summary.budget.maxInferenceCostUsd)
3618
+ ],
3619
+ [
3620
+ "Max inference tokens",
3621
+ summary.budget.maxInferenceTokens === null ? "\u2014" : formatInteger(summary.budget.maxInferenceTokens)
3622
+ ],
3623
+ ["Percent used", formatPercentValue(summary.budget.percentUsed)],
3624
+ ["Exceeded", formatBoolean(summary.budget.exceeded)],
3625
+ ["Warning", formatBoolean(summary.budget.warning)]
3626
+ ]);
3627
+ }
3628
+ function registerMissionResourcesCommand(mission, context, dependencies = {}) {
3629
+ mission.command("resources").description("Show resource usage for a mission").argument("[missionId]").action(async (missionIdValue, _options, command) => {
3630
+ const homeDir = resolveMissionCommandHomeDir(context);
3631
+ const missionId = await resolveMissionId(
3632
+ missionIdValue,
3633
+ context,
3634
+ command,
3635
+ homeDir
3636
+ );
3637
+ const apiClient = await createMissionApiClient(context, command, dependencies);
3638
+ const summary = normalizeMissionResourceSummary(
3639
+ await apiClient.request({
3640
+ path: `/missions/${missionId}/resources`
3641
+ })
3642
+ );
3643
+ renderMissionResourceSummary(context.stdout, summary);
3644
+ });
3088
3645
  }
3089
3646
 
3090
3647
  // src/commands/mission-run.ts
@@ -3095,54 +3652,54 @@ import { CommanderError as CommanderError2 } from "commander";
3095
3652
  // src/planning.ts
3096
3653
  import path4 from "path";
3097
3654
  import {
3098
- confirm as defaultConfirmPrompt,
3655
+ confirm as defaultConfirmPrompt2,
3099
3656
  input as defaultInputPrompt2,
3100
3657
  select as defaultSelectPrompt2
3101
3658
  } from "@inquirer/prompts";
3102
3659
  import ora2 from "ora";
3103
- import pc6 from "picocolors";
3660
+ import pc8 from "picocolors";
3104
3661
  var DEFAULT_ANALYSIS_TIMEOUT_MS = 5 * 60 * 1e3;
3105
3662
  var DEFAULT_POLL_INTERVAL_MS = 5e3;
3106
3663
  var FREE_TEXT_OPTION_ID = "free_text";
3107
- function isRecord5(value) {
3664
+ function isRecord7(value) {
3108
3665
  return value !== null && !Array.isArray(value) && typeof value === "object";
3109
3666
  }
3110
- function asNonEmptyString6(value) {
3667
+ function asNonEmptyString8(value) {
3111
3668
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
3112
3669
  }
3113
3670
  function asStringArray2(value) {
3114
- return Array.isArray(value) ? value.map((item) => asNonEmptyString6(item)).filter((item) => item !== null) : [];
3671
+ return Array.isArray(value) ? value.map((item) => asNonEmptyString8(item)).filter((item) => item !== null) : [];
3115
3672
  }
3116
3673
  function extractQuestions(payload) {
3117
3674
  if (!payload || !Array.isArray(payload.questions)) {
3118
3675
  return [];
3119
3676
  }
3120
- return payload.questions.filter((question) => isRecord5(question)).map((question) => ({
3121
- detailPrompt: asNonEmptyString6(question.detailPrompt) ?? void 0,
3122
- freeTextOptionId: asNonEmptyString6(question.freeTextOptionId) ?? void 0,
3123
- id: asNonEmptyString6(question.id) ?? "question",
3124
- inputDefault: asNonEmptyString6(question.inputDefault) ?? void 0,
3125
- options: Array.isArray(question.options) ? question.options.filter((option) => isRecord5(option)).map((option) => ({
3126
- description: asNonEmptyString6(option.description) ?? void 0,
3127
- id: asNonEmptyString6(option.id) ?? "option",
3128
- label: asNonEmptyString6(option.label) ?? "Option"
3677
+ return payload.questions.filter((question) => isRecord7(question)).map((question) => ({
3678
+ detailPrompt: asNonEmptyString8(question.detailPrompt) ?? void 0,
3679
+ freeTextOptionId: asNonEmptyString8(question.freeTextOptionId) ?? void 0,
3680
+ id: asNonEmptyString8(question.id) ?? "question",
3681
+ inputDefault: asNonEmptyString8(question.inputDefault) ?? void 0,
3682
+ options: Array.isArray(question.options) ? question.options.filter((option) => isRecord7(option)).map((option) => ({
3683
+ description: asNonEmptyString8(option.description) ?? void 0,
3684
+ id: asNonEmptyString8(option.id) ?? "option",
3685
+ label: asNonEmptyString8(option.label) ?? "Option"
3129
3686
  })) : [],
3130
3687
  references: asStringArray2(question.references),
3131
- text: asNonEmptyString6(question.text) ?? "Clarification required."
3688
+ text: asNonEmptyString8(question.text) ?? "Clarification required."
3132
3689
  }));
3133
3690
  }
3134
3691
  function extractMilestones(payload) {
3135
3692
  if (!payload || !Array.isArray(payload.milestones)) {
3136
3693
  return [];
3137
3694
  }
3138
- return payload.milestones.filter((milestone) => isRecord5(milestone)).map((milestone) => ({
3139
- description: asNonEmptyString6(milestone.description) ?? void 0,
3140
- name: asNonEmptyString6(milestone.name) ?? "milestone",
3695
+ return payload.milestones.filter((milestone) => isRecord7(milestone)).map((milestone) => ({
3696
+ description: asNonEmptyString8(milestone.description) ?? void 0,
3697
+ name: asNonEmptyString8(milestone.name) ?? "milestone",
3141
3698
  testableOutcomes: asStringArray2(milestone.testableOutcomes)
3142
3699
  }));
3143
3700
  }
3144
3701
  function looksLikeMissionDraft(value) {
3145
- return isRecord5(value) && typeof value.taskDescription === "string" && Array.isArray(value.milestones) && Array.isArray(value.features) && Array.isArray(value.assertions);
3702
+ return isRecord7(value) && typeof value.taskDescription === "string" && Array.isArray(value.milestones) && Array.isArray(value.features) && Array.isArray(value.assertions);
3146
3703
  }
3147
3704
  function extractDraft(payload) {
3148
3705
  if (!payload) {
@@ -3161,7 +3718,7 @@ function countItems(payload, key) {
3161
3718
  return Array.isArray(value) ? value.length : 0;
3162
3719
  }
3163
3720
  function formatBudgetSummary(draft) {
3164
- if (!isRecord5(draft.config) || !isRecord5(draft.config.budget)) {
3721
+ if (!isRecord7(draft.config) || !isRecord7(draft.config.budget)) {
3165
3722
  return "unknown";
3166
3723
  }
3167
3724
  const budget = draft.config.budget;
@@ -3175,13 +3732,13 @@ function formatBudgetSummary(draft) {
3175
3732
  }
3176
3733
  }
3177
3734
  function extractFailures(error) {
3178
- return error instanceof ApiError && isRecord5(error.payload) ? asStringArray2(error.payload.failures) : [];
3735
+ return error instanceof ApiError && isRecord7(error.payload) ? asStringArray2(error.payload.failures) : [];
3179
3736
  }
3180
3737
  function extractErrorCode(error) {
3181
- if (!(error instanceof ApiError) || !isRecord5(error.payload)) {
3738
+ if (!(error instanceof ApiError) || !isRecord7(error.payload)) {
3182
3739
  return null;
3183
3740
  }
3184
- return asNonEmptyString6(error.payload.error);
3741
+ return asNonEmptyString8(error.payload.error);
3185
3742
  }
3186
3743
  function extractRound(payload) {
3187
3744
  return typeof payload?.round === "number" && Number.isFinite(payload.round) ? payload.round : 1;
@@ -3234,8 +3791,8 @@ async function defaultPromptInput2(options) {
3234
3791
  message: options.message
3235
3792
  });
3236
3793
  }
3237
- async function defaultPromptConfirm(options) {
3238
- return defaultConfirmPrompt({
3794
+ async function defaultPromptConfirm2(options) {
3795
+ return defaultConfirmPrompt2({
3239
3796
  default: options.default,
3240
3797
  message: options.message
3241
3798
  });
@@ -3265,20 +3822,20 @@ async function postDraftApproval(client, sessionId) {
3265
3822
  path: `/plan/${sessionId}/approve`
3266
3823
  });
3267
3824
  }
3268
- function writeSection2(stdout, title) {
3825
+ function writeSection3(stdout, title) {
3269
3826
  writeLine(stdout);
3270
- writeLine(stdout, pc6.bold(title));
3827
+ writeLine(stdout, pc8.bold(title));
3271
3828
  }
3272
3829
  function renderQuestion(stdout, question, index) {
3273
3830
  writeLine(stdout, `${index + 1}. ${question.text}`);
3274
3831
  if (question.references && question.references.length > 0) {
3275
- writeLine(stdout, ` ${pc6.dim(`References: ${question.references.join(", ")}`)}`);
3832
+ writeLine(stdout, ` ${pc8.dim(`References: ${question.references.join(", ")}`)}`);
3276
3833
  }
3277
3834
  }
3278
3835
  function renderMilestones(stdout, milestones, reviewRound) {
3279
- writeSection2(stdout, `Milestone review round ${reviewRound}`);
3836
+ writeSection3(stdout, `Milestone review round ${reviewRound}`);
3280
3837
  milestones.forEach((milestone, index) => {
3281
- writeLine(stdout, `${index + 1}. ${pc6.cyan(milestone.name)}`);
3838
+ writeLine(stdout, `${index + 1}. ${pc8.cyan(milestone.name)}`);
3282
3839
  if (milestone.description) {
3283
3840
  writeLine(stdout, ` ${milestone.description}`);
3284
3841
  }
@@ -3289,14 +3846,14 @@ function renderMilestones(stdout, milestones, reviewRound) {
3289
3846
  });
3290
3847
  }
3291
3848
  function renderDraftSummary(stdout, payload, draft) {
3292
- writeSection2(stdout, "Draft summary");
3849
+ writeSection3(stdout, "Draft summary");
3293
3850
  writeLine(stdout, `Milestones: ${countItems(payload, "milestones") || countItems(draft, "milestones")}`);
3294
3851
  writeLine(stdout, `Features: ${countItems(payload, "features") || countItems(draft, "features")}`);
3295
3852
  writeLine(stdout, `Assertions: ${countItems(payload, "assertions") || countItems(draft, "assertions")}`);
3296
3853
  writeLine(stdout, `Budget: ${formatBudgetSummary(draft)}`);
3297
3854
  }
3298
3855
  function renderFailures(stdout, failures) {
3299
- writeSection2(stdout, "Unresolved clarification items");
3856
+ writeSection3(stdout, "Unresolved clarification items");
3300
3857
  failures.forEach((failure) => {
3301
3858
  writeLine(stdout, `- ${failure}`);
3302
3859
  });
@@ -3467,7 +4024,7 @@ async function resolveClarification(options, sessionId, initialPayload) {
3467
4024
  if (payload.state !== "clarifying") {
3468
4025
  break;
3469
4026
  }
3470
- writeSection2(options.stdout, `Clarification round ${round}`);
4027
+ writeSection3(options.stdout, `Clarification round ${round}`);
3471
4028
  questions.forEach((question, index) => renderQuestion(options.stdout, question, index));
3472
4029
  const prompted = await promptForAnswers(options, questions, round);
3473
4030
  localTranscript.push(...prompted.transcript);
@@ -3488,7 +4045,7 @@ async function resolveClarification(options, sessionId, initialPayload) {
3488
4045
  }
3489
4046
  writeLine(
3490
4047
  options.stdout,
3491
- pc6.yellow("Proceeding because --force is enabled.")
4048
+ pc8.yellow("Proceeding because --force is enabled.")
3492
4049
  );
3493
4050
  payload = await postMilestoneConfirmation(options.client, sessionId, {});
3494
4051
  return {
@@ -3529,7 +4086,7 @@ async function resolveMilestones(options, sessionId, initialPayload) {
3529
4086
  while (true) {
3530
4087
  const milestones = extractMilestones(payload);
3531
4088
  renderMilestones(options.stdout, milestones, reviewRound);
3532
- const confirmed = options.autoApprove ? true : await (options.promptConfirm ?? defaultPromptConfirm)({
4089
+ const confirmed = options.autoApprove ? true : await (options.promptConfirm ?? defaultPromptConfirm2)({
3533
4090
  default: true,
3534
4091
  message: "Do these milestones look correct?"
3535
4092
  });
@@ -3570,7 +4127,7 @@ async function runPlanningFlow(options) {
3570
4127
  if (options.existingMissionDraft) {
3571
4128
  writeLine(
3572
4129
  options.stdout,
3573
- pc6.cyan("Skipping planning because mission-draft.json already exists.")
4130
+ pc8.cyan("Skipping planning because mission-draft.json already exists.")
3574
4131
  );
3575
4132
  return {
3576
4133
  cancelled: false,
@@ -3579,7 +4136,7 @@ async function runPlanningFlow(options) {
3579
4136
  skippedPlanning: true
3580
4137
  };
3581
4138
  }
3582
- const sessionId = asNonEmptyString6(options.existingSessionId) ?? asNonEmptyString6(
4139
+ const sessionId = asNonEmptyString8(options.existingSessionId) ?? asNonEmptyString8(
3583
4140
  (await options.client.request({
3584
4141
  body: {
3585
4142
  repos: options.repoPaths,
@@ -3621,12 +4178,12 @@ async function runPlanningFlow(options) {
3621
4178
  payload
3622
4179
  );
3623
4180
  renderDraftSummary(options.stdout, summaryPayload, draft);
3624
- const approved = options.autoApprove ? true : await (options.promptConfirm ?? defaultPromptConfirm)({
4181
+ const approved = options.autoApprove ? true : await (options.promptConfirm ?? defaultPromptConfirm2)({
3625
4182
  default: true,
3626
4183
  message: "Approve this draft and continue to upload?"
3627
4184
  });
3628
4185
  if (!approved) {
3629
- writeLine(options.stdout, pc6.yellow("Planning cancelled before upload."));
4186
+ writeLine(options.stdout, pc8.yellow("Planning cancelled before upload."));
3630
4187
  return {
3631
4188
  cancelled: true,
3632
4189
  draft,
@@ -3635,7 +4192,7 @@ async function runPlanningFlow(options) {
3635
4192
  };
3636
4193
  }
3637
4194
  await options.persistMissionDraft(draft);
3638
- writeLine(options.stdout, pc6.green("Saved approved mission draft."));
4195
+ writeLine(options.stdout, pc8.green("Saved approved mission draft."));
3639
4196
  return {
3640
4197
  cancelled: false,
3641
4198
  draft,
@@ -3650,7 +4207,7 @@ import { createReadStream as createReadStream2 } from "fs";
3650
4207
  import path6 from "path";
3651
4208
  import { Transform as Transform2 } from "stream";
3652
4209
  import ora3 from "ora";
3653
- import pc7 from "picocolors";
4210
+ import pc9 from "picocolors";
3654
4211
 
3655
4212
  // src/snapshot.ts
3656
4213
  import { execFile } from "child_process";
@@ -4198,13 +4755,13 @@ function defaultCreateSpinner3(text) {
4198
4755
  spinner.start();
4199
4756
  return spinner;
4200
4757
  }
4201
- async function defaultPromptConfirm2(options) {
4758
+ async function defaultPromptConfirm3(options) {
4202
4759
  return confirm({
4203
4760
  default: options.default,
4204
4761
  message: options.message
4205
4762
  });
4206
4763
  }
4207
- function formatBytes(bytes) {
4764
+ function formatBytes2(bytes) {
4208
4765
  if (!Number.isFinite(bytes) || bytes < 1024) {
4209
4766
  return `${bytes} B`;
4210
4767
  }
@@ -4226,7 +4783,7 @@ function formatDuration(milliseconds) {
4226
4783
  function formatRate(bytesTransferred, elapsedMs) {
4227
4784
  const safeElapsedMs = Math.max(elapsedMs, 1);
4228
4785
  const bytesPerSecond = bytesTransferred * 1e3 / safeElapsedMs;
4229
- return `${formatBytes(Math.max(1, Math.round(bytesPerSecond)))}/s`;
4786
+ return `${formatBytes2(Math.max(1, Math.round(bytesPerSecond)))}/s`;
4230
4787
  }
4231
4788
  function formatFinding(finding) {
4232
4789
  const location = finding.line === null ? finding.filePath : `${finding.filePath}:${finding.line}`;
@@ -4247,19 +4804,19 @@ function isTerminalUploadStatus2(status) {
4247
4804
  return status === "uploaded" || status === "blob_exists";
4248
4805
  }
4249
4806
  function renderSafetyGate(stdout, repos, existingBlobs) {
4250
- writeLine(stdout, pc7.bold("Upload safety check"));
4807
+ writeLine(stdout, pc9.bold("Upload safety check"));
4251
4808
  writeLine(stdout, "Repos queued for upload:");
4252
4809
  for (const repo of repos) {
4253
- const cleanliness = repo.manifest.dirty ? pc7.yellow("dirty") : pc7.green("clean");
4254
- const secretSummary = repo.secretFindings.length > 0 ? pc7.red(`${repo.secretFindings.length} finding(s)`) : pc7.green("no findings");
4255
- const dedupSummary = existingBlobs.has(repo.manifest.archiveSha256) ? pc7.cyan("blob already exists remotely") : "new upload";
4810
+ const cleanliness = repo.manifest.dirty ? pc9.yellow("dirty") : pc9.green("clean");
4811
+ const secretSummary = repo.secretFindings.length > 0 ? pc9.red(`${repo.secretFindings.length} finding(s)`) : pc9.green("no findings");
4812
+ const dedupSummary = existingBlobs.has(repo.manifest.archiveSha256) ? pc9.cyan("blob already exists remotely") : "new upload";
4256
4813
  writeLine(
4257
4814
  stdout,
4258
- `- ${pc7.cyan(repo.manifest.repoId)} \u2014 ${repo.manifest.localPath}`
4815
+ `- ${pc9.cyan(repo.manifest.repoId)} \u2014 ${repo.manifest.localPath}`
4259
4816
  );
4260
4817
  writeLine(
4261
4818
  stdout,
4262
- ` Status: ${cleanliness}; archive ${formatBytes(repo.manifest.archiveBytes)}; ${dedupSummary}; secret scan ${secretSummary}`
4819
+ ` Status: ${cleanliness}; archive ${formatBytes2(repo.manifest.archiveBytes)}; ${dedupSummary}; secret scan ${secretSummary}`
4263
4820
  );
4264
4821
  if (repo.warnings.length > 0) {
4265
4822
  for (const warning of repo.warnings) {
@@ -4316,7 +4873,7 @@ async function uploadArchive(options, repo, uploadUrl) {
4316
4873
  const fetchImpl = options.fetch ?? globalThis.fetch;
4317
4874
  const createSpinner = options.createSpinner ?? defaultCreateSpinner3;
4318
4875
  const spinner = createSpinner(
4319
- `Uploading ${repo.manifest.repoId} 0 B / ${formatBytes(repo.manifest.archiveBytes)} (0 B/s)`
4876
+ `Uploading ${repo.manifest.repoId} 0 B / ${formatBytes2(repo.manifest.archiveBytes)} (0 B/s)`
4320
4877
  );
4321
4878
  spinner.start(spinner.text);
4322
4879
  const startedAt = Date.now();
@@ -4325,7 +4882,7 @@ async function uploadArchive(options, repo, uploadUrl) {
4325
4882
  transform(chunk, _encoding, callback) {
4326
4883
  bytesTransferred += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(String(chunk));
4327
4884
  const elapsedMs2 = Date.now() - startedAt;
4328
- spinner.text = `Uploading ${repo.manifest.repoId} ${formatBytes(bytesTransferred)} / ${formatBytes(repo.manifest.archiveBytes)} (${formatRate(bytesTransferred, elapsedMs2)})`;
4885
+ spinner.text = `Uploading ${repo.manifest.repoId} ${formatBytes2(bytesTransferred)} / ${formatBytes2(repo.manifest.archiveBytes)} (${formatRate(bytesTransferred, elapsedMs2)})`;
4329
4886
  callback(null, chunk);
4330
4887
  }
4331
4888
  });
@@ -4356,7 +4913,7 @@ async function uploadArchive(options, repo, uploadUrl) {
4356
4913
  }
4357
4914
  const elapsedMs = Date.now() - startedAt;
4358
4915
  spinner.succeed(
4359
- `Uploaded ${repo.manifest.repoId} ${formatBytes(bytesTransferred)} in ${formatDuration(elapsedMs)} (${formatRate(bytesTransferred, elapsedMs)})`
4916
+ `Uploaded ${repo.manifest.repoId} ${formatBytes2(bytesTransferred)} in ${formatDuration(elapsedMs)} (${formatRate(bytesTransferred, elapsedMs)})`
4360
4917
  );
4361
4918
  }
4362
4919
  async function processRepoUpload(options, repo, remoteLaunchId) {
@@ -4399,7 +4956,7 @@ async function processRepoUpload(options, repo, remoteLaunchId) {
4399
4956
  );
4400
4957
  writeLine(
4401
4958
  options.stdout,
4402
- pc7.cyan(`Skipping upload for ${repo.manifest.repoId}; blob already exists remotely.`)
4959
+ pc9.cyan(`Skipping upload for ${repo.manifest.repoId}; blob already exists remotely.`)
4403
4960
  );
4404
4961
  return;
4405
4962
  }
@@ -4436,7 +4993,7 @@ async function processRepoUpload(options, repo, remoteLaunchId) {
4436
4993
  );
4437
4994
  }
4438
4995
  async function runUploadPipeline(options) {
4439
- const promptConfirm = options.promptConfirm ?? defaultPromptConfirm2;
4996
+ const promptConfirm = options.promptConfirm ?? defaultPromptConfirm3;
4440
4997
  const launchSnapshot = await loadLaunchSnapshot(options.launchId, {
4441
4998
  cwd: options.cwd,
4442
4999
  homeDir: options.homeDir
@@ -4493,7 +5050,7 @@ async function runUploadPipeline(options) {
4493
5050
  })) {
4494
5051
  writeLine(
4495
5052
  options.stdout,
4496
- pc7.yellow("Upload cancelled before creating a remote launch.")
5053
+ pc9.yellow("Upload cancelled before creating a remote launch.")
4497
5054
  );
4498
5055
  return {
4499
5056
  finalized: false,
@@ -4510,7 +5067,7 @@ async function runUploadPipeline(options) {
4510
5067
  if (existingBlobs.size > 0) {
4511
5068
  writeLine(
4512
5069
  options.stdout,
4513
- pc7.cyan(
5070
+ pc9.cyan(
4514
5071
  `Remote dedup will reuse ${existingBlobs.size} existing blob${existingBlobs.size === 1 ? "" : "s"}.`
4515
5072
  )
4516
5073
  );
@@ -4537,7 +5094,7 @@ async function runUploadPipeline(options) {
4537
5094
  if (isTerminalUploadStatus2(uploadStates.get(repo.manifest.repoId))) {
4538
5095
  writeLine(
4539
5096
  options.stdout,
4540
- pc7.cyan(`Skipping ${repo.manifest.repoId}; already completed in upload-state.json.`)
5097
+ pc9.cyan(`Skipping ${repo.manifest.repoId}; already completed in upload-state.json.`)
4541
5098
  );
4542
5099
  continue;
4543
5100
  }
@@ -4555,7 +5112,7 @@ async function runUploadPipeline(options) {
4555
5112
  failures.push(repo.manifest.repoId);
4556
5113
  writeLine(
4557
5114
  options.stdout,
4558
- pc7.red(
5115
+ pc9.red(
4559
5116
  error instanceof Error ? error.message : `Upload failed for ${repo.manifest.repoId}.`
4560
5117
  )
4561
5118
  );
@@ -4577,7 +5134,7 @@ async function runUploadPipeline(options) {
4577
5134
  }
4578
5135
  writeLine(
4579
5136
  options.stdout,
4580
- pc7.green(`Upload complete. Remote launch ${remoteLaunchId} is finalized.`)
5137
+ pc9.green(`Upload complete. Remote launch ${remoteLaunchId} is finalized.`)
4581
5138
  );
4582
5139
  return {
4583
5140
  finalized: true,
@@ -4752,10 +5309,10 @@ function registerMissionRunCommand(mission, context, dependencies = {}) {
4752
5309
  }
4753
5310
 
4754
5311
  // src/commands/mission-status.ts
4755
- import pc8 from "picocolors";
4756
- function writeSection3(stdout, title) {
5312
+ import pc10 from "picocolors";
5313
+ function writeSection4(stdout, title) {
4757
5314
  writeLine(stdout);
4758
- writeLine(stdout, pc8.bold(title));
5315
+ writeLine(stdout, pc10.bold(title));
4759
5316
  }
4760
5317
  function normalizeMilestoneOrder(milestones, features, assertions) {
4761
5318
  const known = new Map(milestones.map((milestone) => [milestone.name, milestone]));
@@ -4828,7 +5385,7 @@ function renderMissionOverview(stdout, missionState) {
4828
5385
  );
4829
5386
  }
4830
5387
  function renderActiveWorker(stdout, missionState) {
4831
- writeSection3(stdout, "Active worker");
5388
+ writeSection4(stdout, "Active worker");
4832
5389
  writeLine(stdout, `Feature: ${missionState.currentFeatureId ?? "none"}`);
4833
5390
  writeLine(
4834
5391
  stdout,
@@ -4839,7 +5396,7 @@ function renderBudget(stdout, missionState) {
4839
5396
  if (missionState.estimatedCostUsd === null && missionState.inferenceTokensUsed === null && missionState.sandboxMinutesUsed === null) {
4840
5397
  return;
4841
5398
  }
4842
- writeSection3(stdout, "Budget / usage");
5399
+ writeSection4(stdout, "Budget / usage");
4843
5400
  writeLine(stdout, `Estimated cost: ${formatCurrency(missionState.estimatedCostUsd)}`);
4844
5401
  writeLine(
4845
5402
  stdout,
@@ -4851,11 +5408,11 @@ function renderBudget(stdout, missionState) {
4851
5408
  );
4852
5409
  }
4853
5410
  function renderMilestones2(stdout, milestones, features) {
4854
- writeSection3(stdout, "Milestones");
5411
+ writeSection4(stdout, "Milestones");
4855
5412
  for (const milestone of milestones) {
4856
5413
  writeLine(
4857
5414
  stdout,
4858
- `- ${pc8.cyan(milestone.name)} (${milestone.state}) \xB7 ${milestone.completedFeatureCount}/${milestone.featureCount} features \xB7 ${milestone.passedAssertionCount}/${milestone.assertionCount} assertions`
5415
+ `- ${pc10.cyan(milestone.name)} (${milestone.state}) \xB7 ${milestone.completedFeatureCount}/${milestone.featureCount} features \xB7 ${milestone.passedAssertionCount}/${milestone.assertionCount} assertions`
4859
5416
  );
4860
5417
  const milestoneFeatures = features.filter(
4861
5418
  (feature) => feature.milestone === milestone.name
@@ -4866,7 +5423,7 @@ function renderMilestones2(stdout, milestones, features) {
4866
5423
  }
4867
5424
  }
4868
5425
  function renderAssertionSummary(stdout, milestones, assertions) {
4869
- writeSection3(stdout, "Assertion pass rates");
5426
+ writeSection4(stdout, "Assertion pass rates");
4870
5427
  const overall = summarizeAssertions2(assertions);
4871
5428
  writeLine(
4872
5429
  stdout,
@@ -4917,6 +5474,7 @@ function registerMissionCommands(program, context, dependencies = {}) {
4917
5474
  registerMissionStatusCommand(mission, context, dependencies);
4918
5475
  registerMissionInfoCommand(mission, context, dependencies);
4919
5476
  registerMissionListCommand(mission, context, dependencies);
5477
+ registerMissionResourcesCommand(mission, context, dependencies);
4920
5478
  registerMissionLifecycleCommands(mission, context, dependencies);
4921
5479
  }
4922
5480
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coresource/hz",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "hz": "dist/hz.mjs"
@@ -14,7 +14,7 @@
14
14
  "scripts": {
15
15
  "build": "tsup",
16
16
  "prepublishOnly": "npm run build",
17
- "test": "vitest run",
17
+ "test": "vitest run --sequence.concurrent false",
18
18
  "typecheck": "tsc --noEmit"
19
19
  },
20
20
  "dependencies": {