1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
use std::path::PathBuf;
use carryctx::application::runtime::InvocationContext;
use carryctx::error::{CarryCtxError, ExitCode};
use clap::Parser;
// ── Export (ctxpack dir v1) ────────────────────────────────────────────
/// Offline-first portable export of project state (`ctxpack-dir` v1).
///
/// The bundle-format flag is deliberately `--pack-format`, not `--format`:
/// the root CLI already defines a global `--format` (output style
/// text|json|markdown) and a same-named flag here collides inside this scope
/// (see the `graph export --type` precedent and the CLI-integrity test in
/// main.rs). `--dry-run` arrives via the global flag and prints the plan
/// (entity counts, target path) without writing anything.
#[derive(Parser, Debug)]
pub struct PackArgs {
/// Bundle layout format (v1 supports only `dir`)
#[arg(long, default_value = "dir")]
pub pack_format: String,
/// Output directory for the export bundle
#[arg(short, long)]
pub output: Option<PathBuf>,
/// Stream a tar archive of the bundle to stdout (unsupported in v1)
#[arg(long)]
pub stdout: bool,
}
// ═══════════════════════════════════════════════════════════════════════════
// Handler: export
// ═══════════════════════════════════════════════════════════════════════════
pub fn handle_export(
args: &PackArgs,
ctx: &InvocationContext,
is_json: bool,
) -> Result<ExitCode, ExitCode> {
// v1 ships no in-binary tar writer (offline, no new dependencies):
// fail closed with UNSUPPORTED_OPERATION instead of a corrupt stream.
if args.stdout {
return crate::render_and_print(
"export.create",
Err::<serde_json::Value, _>(CarryCtxError::unsupported_operation(
"Export '--stdout' tar streaming is not supported in v1: the binary ships no tar writer. Export with '-o <dir>' and stream externally, e.g. 'tar -cf - <dir> | ssh ...'.",
)),
is_json,
ctx.quiet,
);
}
let work_dir = crate::resolve_work_dir(ctx);
if ctx.dry_run {
let result = require_output(args).and_then(|out| {
carryctx::application::export::plan_export(work_dir, &args.pack_format, &out)
});
if !ctx.quiet {
if let Ok(data) = &result {
let rows: u64 = data
.get("counts")
.and_then(|c| c.as_object())
.map(|counts| counts.values().filter_map(|v| v.as_u64()).sum())
.unwrap_or(0);
eprintln!(
"[dry-run] Would export {rows} rows to '{}'; nothing written.",
data.get("path").and_then(|p| p.as_str()).unwrap_or("?")
);
}
}
return crate::render_and_print("export.create", result, is_json, ctx.quiet);
}
let result = require_output(args).and_then(|out| {
carryctx::application::export::run_export(
work_dir,
&args.pack_format,
&out,
ctx.agent.clone(),
ctx.session.clone(),
)
});
crate::render_and_print("export.create", result, is_json, ctx.quiet)
}
fn require_output(args: &PackArgs) -> Result<PathBuf, CarryCtxError> {
args.output.clone().ok_or_else(|| {
CarryCtxError::invalid_arguments(
"Missing export target: pass '-o <dir>' (or '--stdout' for a tar stream).",
)
})
}