@tiangong-lca/cli 0.0.19 → 0.0.22

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/src/cli.js CHANGED
@@ -54,6 +54,10 @@ import { runDatasetPatchApply, } from './lib/dataset-patch.js';
54
54
  import { runDatasetSaveDraft, } from './lib/dataset-save-draft-run.js';
55
55
  import { runDatasetClassificationApply, runDatasetClassificationAudit, runDatasetClassificationChildren, runDatasetClassificationPath, } from './lib/dataset-classification.js';
56
56
  import { runDatasetMaintenanceClearAccount, } from './lib/dataset-maintenance-clear-account.js';
57
+ import { runDatasetMaintenancePlan } from './lib/dataset-maintenance-plan.js';
58
+ import { runDatasetMaintenanceApply } from './lib/dataset-maintenance-apply.js';
59
+ import { runDatasetMaintenanceVerify } from './lib/dataset-maintenance-verify.js';
60
+ import { runDatasetSourceUploadAttachments, } from './lib/dataset-source-upload-attachments.js';
57
61
  function renderMainHelp(dotEnvStatus) {
58
62
  return `TianGong LCA CLI
59
63
 
@@ -73,7 +77,7 @@ Implemented Commands:
73
77
  doctor show environment diagnostics
74
78
  search flow | process | lifecyclemodel
75
79
  process get | list | identity-preflight | build-plan | scope-statistics | dedup-review | auto-build | resume-build | publish-build | complete-required-fields | save-draft | batch-build | refresh-references | verify-rows
76
- dataset contract get | context-pack | classification children/path/audit/apply | curation-queue build/next/verify | import-lca convert | author | patch apply | save-draft | validate | verify-remote | bilingual extract/apply/validate | evidence-search plan/run | references rewrite/refresh-remote | maintenance clear-account
80
+ dataset contract get | context-pack | classification children/path/audit/apply | curation-queue build/next/verify | import-lca convert | author | patch apply | save-draft | source upload-attachments | validate | verify-remote | bilingual extract/apply/validate | evidence-search plan/run | references rewrite/refresh-remote | maintenance clear-account/plan/apply/verify
77
81
  flow get | list | identity-preflight | build-plan | fetch-rows | materialize-decisions | remediate | publish-version | publish-reviewed-data | build-alias-map | scan-process-flow-refs | plan-process-flow-repairs | apply-process-flow-repairs | regen-product | validate-processes
78
82
  lifecyclemodel auto-build | validate-build | publish-build | save-draft | graph | build-resulting-process | publish-resulting-process | orchestrate
79
83
  qa process | flow | lifecyclemodel
@@ -84,7 +88,6 @@ Implemented Commands:
84
88
  Planned Surface (not implemented yet):
85
89
  auth whoami | doctor-auth
86
90
  job get | wait | logs
87
- dataset maintenance plan | apply | verify
88
91
 
89
92
  Planned commands currently print an explicit "not implemented yet" message and exit with code 2.
90
93
 
@@ -127,7 +130,7 @@ Examples:
127
130
  tiangong-lca dataset evidence-search run --input ./evidence-search.request.json --results ./search-results.json --out-dir ./evidence-search
128
131
  tiangong-lca dataset references rewrite --input ./rows.jsonl --from flow:<old-id>@<old-version> --to flow:<new-id>@<new-version> --out-dir /abs/path/to/dataset-rewrite
129
132
  tiangong-lca dataset maintenance clear-account --out-dir /abs/path/to/account-clear --json
130
- tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation redo-import --out-dir /abs/path/to/dataset-maintenance
133
+ tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation merge-support-aliases --out-dir /abs/path/to/dataset-maintenance
131
134
  tiangong-lca lifecyclemodel auto-build --input ./lifecyclemodel-auto-build.request.json --out-dir /abs/path/to/lifecyclemodel-run
132
135
  tiangong-lca lifecyclemodel validate-build --run-dir /abs/path/to/lifecyclemodel-run
133
136
  tiangong-lca lifecyclemodel publish-build --run-dir /abs/path/to/lifecyclemodel-run
@@ -268,6 +271,7 @@ Implemented Subcommands:
268
271
  author Extract source evidence and prepare TIDAS context packs for AI authoring
269
272
  patch apply Apply AI-authored structured dataset patches deterministically
270
273
  save-draft Save contact/source/support or other canonical dataset rows through the platform dataset command path
274
+ source upload-attachments Upload source referenceToDigitalFile binaries to storage and rewrite their @uri
271
275
  validate Validate local flow / process / lifecyclemodel rows with the TIDAS SDK
272
276
  verify-remote Verify dataset roots and TIDAS references against remote published versions
273
277
  bilingual extract Extract bilingual translation units from local rows
@@ -277,9 +281,9 @@ Implemented Subcommands:
277
281
  references rewrite Rewrite flow references in local process and lifecyclemodel rows
278
282
  references refresh-remote Refresh local TIDAS reference versions to latest reachable remote rows
279
283
  maintenance clear-account Dry-run or clear current authenticated account-owned dataset rows through RLS
280
-
281
- Planned Subcommands:
282
- maintenance plan/apply/verify Plan, execute, and verify RLS-scoped dataset delete/redo maintenance
284
+ maintenance plan Build an immutable, RLS-visible row-level maintenance plan
285
+ maintenance apply Execute an explicitly approved maintenance plan through current-user RLS
286
+ maintenance verify Read back affected rows and references against the immutable plan
283
287
 
284
288
  Examples:
285
289
  tiangong-lca dataset contract get --type process --include schema,methodology,ruleset --out-dir ./contract --help
@@ -305,25 +309,23 @@ Examples:
305
309
  tiangong-lca dataset references rewrite --input ./rows.jsonl --from flow:<old-id>@<old-version> --to flow:<new-id>@<new-version> --out-dir ./dataset-rewrite --help
306
310
  tiangong-lca dataset references refresh-remote --input ./rows.jsonl --out ./rows.refreshed.jsonl --out-dir ./dataset-reference-refresh --help
307
311
  tiangong-lca dataset maintenance clear-account --out-dir ./account-clear --json --help
308
- tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation redo-import --out-dir ./dataset-maintenance --help
312
+ tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation merge-support-aliases --out-dir ./dataset-maintenance --help
309
313
  `.trim();
310
314
  }
311
315
  function renderDatasetMaintenanceHelp() {
312
316
  return `Usage:
313
317
  tiangong-lca dataset maintenance <clear-account|plan|apply|verify> [options]
314
318
 
315
- Status:
316
- clear-account is implemented for current authenticated account cleanup.
317
- plan/apply/verify remain reserved for row-level RLS-scoped cleanup and redo workflows and currently exit with code 2 for executable actions.
318
-
319
- Implemented Actions:
319
+ Actions:
320
320
  clear-account Dry-run or delete current authenticated account-owned lifecyclemodels, processes, flows, sources, and contacts.
321
-
322
- Planned Actions:
323
321
  plan Build an immutable maintenance plan from a scope manifest, visible remote snapshot, dependency impact report, and intended operation.
324
322
  apply Execute an approved plan through current-user RLS and platform dataset command paths; never bypass RLS or delete rows outside the visible scope.
325
323
  verify Re-fetch affected rows and references, then prove that deleted, updated, skipped, protected, and redone rows match the plan.
326
324
 
325
+ Safety:
326
+ plan and verify are read-only.
327
+ apply is commit-only and requires --commit, the exact plan SHA-256 via --approve-plan, and the current account email via --confirm.
328
+
327
329
  Required Artifact Contract:
328
330
  - maintenance-plan.json
329
331
  - rls-visible-snapshot.json
@@ -336,11 +338,66 @@ Required Artifact Contract:
336
338
  Examples:
337
339
  tiangong-lca dataset maintenance clear-account --out-dir ./account-clear --json
338
340
  tiangong-lca dataset maintenance clear-account --commit --confirm user@example.com --out-dir ./account-clear
339
- tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation redo-import --out-dir ./dataset-maintenance
340
- tiangong-lca dataset maintenance apply --plan ./dataset-maintenance/maintenance-plan.json --commit
341
+ tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation merge-support-aliases --out-dir ./dataset-maintenance
342
+ tiangong-lca dataset maintenance apply --plan ./dataset-maintenance/maintenance-plan.json --commit --approve-plan <sha256> --confirm user@example.com
341
343
  tiangong-lca dataset maintenance verify --plan ./dataset-maintenance/maintenance-plan.json --out-dir ./dataset-maintenance/verify
342
344
  `.trim();
343
345
  }
346
+ function renderDatasetMaintenancePlanHelp() {
347
+ return `Usage:
348
+ tiangong-lca dataset maintenance plan --scope <file> --operation <operation> --out-dir <dir> [options]
349
+
350
+ Operations:
351
+ delete | retire | redo-import | repair-references | merge-support-aliases
352
+
353
+ Options:
354
+ --scope <file> Maintenance scope manifest
355
+ --operation <value> Intended row-level maintenance operation
356
+ --out-dir <dir> Artifact directory
357
+ --page-size <n> Snapshot page size
358
+ --timeout-ms <n> Request timeout in milliseconds
359
+ --json Print compact JSON
360
+ -h, --help
361
+
362
+ Outputs written under --out-dir include the immutable maintenance-plan.json, visible snapshot,
363
+ protected-row ledger, reference-impact report, and dry-run report.
364
+ `.trim();
365
+ }
366
+ function renderDatasetMaintenanceApplyHelp() {
367
+ return `Usage:
368
+ tiangong-lca dataset maintenance apply --plan <file> --commit --approve-plan <sha256> --confirm <email> [options]
369
+
370
+ Behavior:
371
+ Commit-only. The command rejects dry-run mode, a missing --commit flag, a plan hash mismatch,
372
+ or a confirmation email that does not match the current authenticated account.
373
+
374
+ Options:
375
+ --plan <file> Immutable maintenance-plan.json
376
+ --commit Execute the approved plan
377
+ --approve-plan <sha256> Exact SHA-256 recorded for the plan
378
+ --confirm <email> Current authenticated account email
379
+ --timeout-ms <n> Request timeout in milliseconds
380
+ --json Print compact JSON
381
+ -h, --help
382
+
383
+ Outputs include approval-record.json and commit-report.json alongside an append-only action ledger.
384
+ `.trim();
385
+ }
386
+ function renderDatasetMaintenanceVerifyHelp() {
387
+ return `Usage:
388
+ tiangong-lca dataset maintenance verify --plan <file> [options]
389
+
390
+ Options:
391
+ --plan <file> Immutable maintenance-plan.json
392
+ --out-dir <dir> Optional verification artifact directory
393
+ --page-size <n> Readback page size
394
+ --timeout-ms <n> Request timeout in milliseconds
395
+ --json Print compact JSON
396
+ -h, --help
397
+
398
+ Outputs include readback-verify-report.json with affected-row and reference checks.
399
+ `.trim();
400
+ }
344
401
  function renderDatasetMaintenanceClearAccountHelp() {
345
402
  return `Usage:
346
403
  tiangong-lca dataset maintenance clear-account [options]
@@ -518,7 +575,7 @@ function renderDatasetImportLcaHelp() {
518
575
  Options:
519
576
  --input <path> Source file, directory, or package to import
520
577
  --output-dir <dir> Output directory for generated package and reports
521
- --from-format <format> auto, ecospold1, ecospold2, openlca-jsonld, openlca-process-xlsx, simapro-csv
578
+ --from-format <format> auto, ecospold1, ecospold2, openlca-jsonld, openlca-process-xlsx, simapro-csv, ilcd
522
579
  --target <target> tidas | ilcd | both (default: tidas)
523
580
  --report <file> Conversion report path (default: <output-dir>/conversion-report.json)
524
581
  --mapping-dir <dir> Optional custom mapping/reference data directory
@@ -644,6 +701,33 @@ Contract:
644
701
  Process and lifecyclemodel imports may still use their dedicated save-draft commands when the workflow needs their specialized reports.
645
702
  `.trim();
646
703
  }
704
+ function renderDatasetSourceUploadAttachmentsHelp() {
705
+ return `Usage:
706
+ tiangong-lca dataset source upload-attachments --input <file|dir> --external-docs-dir <dir> [options]
707
+
708
+ Uploads the binary documents referenced by source datasets via referenceToDigitalFile
709
+ (e.g. "../external_docs/pef_method.pdf") to a Supabase Storage bucket using the current
710
+ authenticated account session, then rewrites each source's referenceToDigitalFile @uri to
711
+ "../<bucket>/<key>". Plain http(s) URIs are left untouched; backslash paths and filename
712
+ case differences are normalized; each physical file is uploaded once (deduped).
713
+
714
+ Options:
715
+ --input <file|dir> Source TIDAS rows (.jsonl, .json, or a directory of *.json)
716
+ --external-docs-dir <dir> Directory holding the referenced binaries
717
+ --bucket <name> Storage bucket (default: external_docs)
718
+ --out-dir <dir> Artifact directory
719
+ --commit Upload binaries (default: dry-run plan only)
720
+ --dry-run Plan without uploading (default)
721
+ --verify After upload, create a signed URL to confirm each object resolves
722
+ --timeout-ms <n> Per-request timeout (default: 30000)
723
+ --json Print compact JSON
724
+ -h, --help
725
+
726
+ Outputs written under --out-dir:
727
+ - attachments-report.json
728
+ - rewritten-sources.jsonl
729
+ `.trim();
730
+ }
647
731
  function renderDatasetValidateHelp() {
648
732
  return `Usage:
649
733
  tiangong-lca dataset validate --input <file> [options]
@@ -2096,6 +2180,62 @@ function parseDatasetSaveDraftFlags(args) {
2096
2180
  allowReferenceOnlySupport: Boolean(values['allow-account-local-support']),
2097
2181
  };
2098
2182
  }
2183
+ function parseDatasetSourceUploadAttachmentsFlags(args) {
2184
+ let values;
2185
+ try {
2186
+ ({ values } = parseArgs({
2187
+ args,
2188
+ allowPositionals: false,
2189
+ strict: true,
2190
+ options: {
2191
+ help: { type: 'boolean', short: 'h' },
2192
+ json: { type: 'boolean' },
2193
+ input: { type: 'string' },
2194
+ 'external-docs-dir': { type: 'string' },
2195
+ bucket: { type: 'string' },
2196
+ 'out-dir': { type: 'string' },
2197
+ commit: { type: 'boolean' },
2198
+ 'dry-run': { type: 'boolean' },
2199
+ verify: { type: 'boolean' },
2200
+ 'timeout-ms': { type: 'string' },
2201
+ },
2202
+ }));
2203
+ }
2204
+ catch (error) {
2205
+ throw new CliError(String(error), {
2206
+ code: 'INVALID_ARGS',
2207
+ exitCode: 2,
2208
+ });
2209
+ }
2210
+ if (values.commit && values['dry-run']) {
2211
+ throw new CliError('Cannot pass both --commit and --dry-run.', {
2212
+ code: 'INVALID_DATASET_SOURCE_UPLOAD_MODE',
2213
+ exitCode: 2,
2214
+ });
2215
+ }
2216
+ let timeoutMs;
2217
+ if (typeof values['timeout-ms'] === 'string') {
2218
+ const parsed = Number.parseInt(values['timeout-ms'], 10);
2219
+ if (!Number.isInteger(parsed)) {
2220
+ throw new CliError('--timeout-ms must be an integer.', {
2221
+ code: 'INVALID_ARGS',
2222
+ exitCode: 2,
2223
+ });
2224
+ }
2225
+ timeoutMs = parsed;
2226
+ }
2227
+ return {
2228
+ help: Boolean(values.help),
2229
+ json: Boolean(values.json),
2230
+ inputPath: typeof values.input === 'string' ? values.input : '',
2231
+ externalDocsDir: typeof values['external-docs-dir'] === 'string' ? values['external-docs-dir'] : '',
2232
+ bucket: typeof values.bucket === 'string' ? values.bucket : null,
2233
+ outDir: typeof values['out-dir'] === 'string' ? values['out-dir'] : null,
2234
+ commit: Boolean(values.commit),
2235
+ verify: Boolean(values.verify),
2236
+ timeoutMs,
2237
+ };
2238
+ }
2099
2239
  function parseDatasetContractFlags(args) {
2100
2240
  let values;
2101
2241
  try {
@@ -2881,6 +3021,130 @@ function parseDatasetMaintenanceClearAccountFlags(args) {
2881
3021
  confirm: typeof values.confirm === 'string' ? values.confirm : null,
2882
3022
  };
2883
3023
  }
3024
+ function parseDatasetMaintenancePositiveInteger(value, flagName) {
3025
+ if (typeof value !== 'string') {
3026
+ return undefined;
3027
+ }
3028
+ if (!/^\d+$/u.test(value.trim()) || Number.parseInt(value.trim(), 10) <= 0) {
3029
+ throw new CliError(`${flagName} must be a positive integer.`, {
3030
+ code: 'DATASET_MAINTENANCE_INTEGER_INVALID',
3031
+ exitCode: 2,
3032
+ details: { flag: flagName, value },
3033
+ });
3034
+ }
3035
+ return Number.parseInt(value.trim(), 10);
3036
+ }
3037
+ function parseDatasetMaintenancePlanFlags(args) {
3038
+ let values;
3039
+ try {
3040
+ ({ values } = parseArgs({
3041
+ args,
3042
+ allowPositionals: false,
3043
+ strict: true,
3044
+ options: {
3045
+ help: { type: 'boolean', short: 'h' },
3046
+ json: { type: 'boolean' },
3047
+ scope: { type: 'string' },
3048
+ operation: { type: 'string' },
3049
+ 'out-dir': { type: 'string' },
3050
+ 'page-size': { type: 'string' },
3051
+ 'timeout-ms': { type: 'string' },
3052
+ },
3053
+ }));
3054
+ }
3055
+ catch (error) {
3056
+ throw new CliError(String(error), {
3057
+ code: 'INVALID_ARGS',
3058
+ exitCode: 2,
3059
+ });
3060
+ }
3061
+ const rawOperation = typeof values.operation === 'string' ? values.operation : null;
3062
+ if (rawOperation !== null &&
3063
+ !['delete', 'retire', 'redo-import', 'repair-references', 'merge-support-aliases'].includes(rawOperation)) {
3064
+ throw new CliError("--operation must be 'delete', 'retire', 'redo-import', 'repair-references', or 'merge-support-aliases'.", {
3065
+ code: 'DATASET_MAINTENANCE_OPERATION_INVALID',
3066
+ exitCode: 2,
3067
+ details: rawOperation,
3068
+ });
3069
+ }
3070
+ return {
3071
+ help: Boolean(values.help),
3072
+ json: Boolean(values.json),
3073
+ scopePath: typeof values.scope === 'string' ? values.scope : '',
3074
+ operation: rawOperation,
3075
+ outDir: typeof values['out-dir'] === 'string' ? values['out-dir'] : '',
3076
+ pageSize: parseDatasetMaintenancePositiveInteger(values['page-size'], '--page-size'),
3077
+ timeoutMs: parseDatasetMaintenancePositiveInteger(values['timeout-ms'], '--timeout-ms'),
3078
+ };
3079
+ }
3080
+ function parseDatasetMaintenanceApplyFlags(args) {
3081
+ let values;
3082
+ try {
3083
+ ({ values } = parseArgs({
3084
+ args,
3085
+ allowPositionals: false,
3086
+ strict: true,
3087
+ options: {
3088
+ help: { type: 'boolean', short: 'h' },
3089
+ json: { type: 'boolean' },
3090
+ plan: { type: 'string' },
3091
+ commit: { type: 'boolean' },
3092
+ 'approve-plan': { type: 'string' },
3093
+ confirm: { type: 'string' },
3094
+ 'timeout-ms': { type: 'string' },
3095
+ 'dry-run': { type: 'boolean' },
3096
+ },
3097
+ }));
3098
+ }
3099
+ catch (error) {
3100
+ throw new CliError(String(error), {
3101
+ code: 'INVALID_ARGS',
3102
+ exitCode: 2,
3103
+ });
3104
+ }
3105
+ return {
3106
+ help: Boolean(values.help),
3107
+ json: Boolean(values.json),
3108
+ planPath: typeof values.plan === 'string' ? values.plan : '',
3109
+ commit: Boolean(values.commit),
3110
+ approvePlan: typeof values['approve-plan'] === 'string' ? values['approve-plan'] : '',
3111
+ confirm: typeof values.confirm === 'string' ? values.confirm : '',
3112
+ timeoutMs: parseDatasetMaintenancePositiveInteger(values['timeout-ms'], '--timeout-ms'),
3113
+ dryRun: Boolean(values['dry-run']),
3114
+ };
3115
+ }
3116
+ function parseDatasetMaintenanceVerifyFlags(args) {
3117
+ let values;
3118
+ try {
3119
+ ({ values } = parseArgs({
3120
+ args,
3121
+ allowPositionals: false,
3122
+ strict: true,
3123
+ options: {
3124
+ help: { type: 'boolean', short: 'h' },
3125
+ json: { type: 'boolean' },
3126
+ plan: { type: 'string' },
3127
+ 'out-dir': { type: 'string' },
3128
+ 'page-size': { type: 'string' },
3129
+ 'timeout-ms': { type: 'string' },
3130
+ },
3131
+ }));
3132
+ }
3133
+ catch (error) {
3134
+ throw new CliError(String(error), {
3135
+ code: 'INVALID_ARGS',
3136
+ exitCode: 2,
3137
+ });
3138
+ }
3139
+ return {
3140
+ help: Boolean(values.help),
3141
+ json: Boolean(values.json),
3142
+ planPath: typeof values.plan === 'string' ? values.plan : '',
3143
+ outDir: typeof values['out-dir'] === 'string' ? values['out-dir'] : undefined,
3144
+ pageSize: parseDatasetMaintenancePositiveInteger(values['page-size'], '--page-size'),
3145
+ timeoutMs: parseDatasetMaintenancePositiveInteger(values['timeout-ms'], '--timeout-ms'),
3146
+ };
3147
+ }
2884
3148
  function parseIdentityPreflightFlags(args) {
2885
3149
  let values;
2886
3150
  try {
@@ -4626,6 +4890,10 @@ export async function executeCli(argv, deps) {
4626
4890
  const datasetClassificationAuditImpl = deps.runDatasetClassificationAuditImpl ?? runDatasetClassificationAudit;
4627
4891
  const datasetClassificationApplyImpl = deps.runDatasetClassificationApplyImpl ?? runDatasetClassificationApply;
4628
4892
  const datasetMaintenanceClearAccountImpl = deps.runDatasetMaintenanceClearAccountImpl ?? runDatasetMaintenanceClearAccount;
4893
+ const datasetMaintenancePlanImpl = deps.runDatasetMaintenancePlanImpl ?? runDatasetMaintenancePlan;
4894
+ const datasetMaintenanceApplyImpl = deps.runDatasetMaintenanceApplyImpl ?? runDatasetMaintenanceApply;
4895
+ const datasetMaintenanceVerifyImpl = deps.runDatasetMaintenanceVerifyImpl ?? runDatasetMaintenanceVerify;
4896
+ const datasetSourceUploadAttachmentsImpl = deps.runDatasetSourceUploadAttachmentsImpl ?? runDatasetSourceUploadAttachments;
4629
4897
  if (flags.version) {
4630
4898
  return { exitCode: 0, stdout: `${loadCliPackageVersion(import.meta.url)}\n`, stderr: '' };
4631
4899
  }
@@ -4947,6 +5215,58 @@ export async function executeCli(argv, deps) {
4947
5215
  stderr: '',
4948
5216
  };
4949
5217
  }
5218
+ if (command === 'dataset' && subcommand === 'source') {
5219
+ const action = commandArgs[0] ?? '';
5220
+ if (!action || action === '--help' || action === '-h') {
5221
+ return {
5222
+ exitCode: 0,
5223
+ stdout: `${renderDatasetSourceUploadAttachmentsHelp()}\n`,
5224
+ stderr: '',
5225
+ };
5226
+ }
5227
+ if (action !== 'upload-attachments') {
5228
+ throw new CliError("dataset source action must be 'upload-attachments'.", {
5229
+ code: 'DATASET_SOURCE_ACTION_INVALID',
5230
+ exitCode: 2,
5231
+ });
5232
+ }
5233
+ const datasetFlags = parseDatasetSourceUploadAttachmentsFlags(commandArgs.slice(1));
5234
+ if (datasetFlags.help) {
5235
+ return {
5236
+ exitCode: 0,
5237
+ stdout: `${renderDatasetSourceUploadAttachmentsHelp()}\n`,
5238
+ stderr: '',
5239
+ };
5240
+ }
5241
+ if (!datasetFlags.inputPath) {
5242
+ throw new CliError('dataset source upload-attachments requires --input.', {
5243
+ code: 'DATASET_SOURCE_UPLOAD_INPUT_REQUIRED',
5244
+ exitCode: 2,
5245
+ });
5246
+ }
5247
+ if (!datasetFlags.externalDocsDir) {
5248
+ throw new CliError('dataset source upload-attachments requires --external-docs-dir.', {
5249
+ code: 'DATASET_SOURCE_UPLOAD_EXTERNAL_DOCS_DIR_REQUIRED',
5250
+ exitCode: 2,
5251
+ });
5252
+ }
5253
+ const report = await datasetSourceUploadAttachmentsImpl({
5254
+ inputPath: datasetFlags.inputPath,
5255
+ externalDocsDir: datasetFlags.externalDocsDir,
5256
+ bucket: datasetFlags.bucket,
5257
+ outDir: datasetFlags.outDir,
5258
+ commit: datasetFlags.commit,
5259
+ verify: datasetFlags.verify,
5260
+ timeoutMs: datasetFlags.timeoutMs,
5261
+ env: deps.env,
5262
+ fetchImpl: deps.fetchImpl,
5263
+ });
5264
+ return {
5265
+ exitCode: report.status === 'completed_with_failures' ? 1 : 0,
5266
+ stdout: stringifyJson(report, datasetFlags.json),
5267
+ stderr: '',
5268
+ };
5269
+ }
4950
5270
  if (command === 'dataset' && subcommand === 'save-draft') {
4951
5271
  const datasetFlags = parseDatasetSaveDraftFlags(commandArgs);
4952
5272
  if (datasetFlags.help) {
@@ -5182,13 +5502,141 @@ export async function executeCli(argv, deps) {
5182
5502
  stderr: '',
5183
5503
  };
5184
5504
  }
5185
- if (!['plan', 'apply', 'verify'].includes(action)) {
5186
- throw new CliError("dataset maintenance action must be 'clear-account', 'plan', 'apply', or 'verify'.", {
5187
- code: 'DATASET_MAINTENANCE_ACTION_INVALID',
5188
- exitCode: 2,
5505
+ if (action === 'plan') {
5506
+ const datasetFlags = parseDatasetMaintenancePlanFlags(commandArgs.slice(1));
5507
+ if (datasetFlags.help) {
5508
+ return {
5509
+ exitCode: 0,
5510
+ stdout: `${renderDatasetMaintenancePlanHelp()}\n`,
5511
+ stderr: '',
5512
+ };
5513
+ }
5514
+ if (!datasetFlags.scopePath) {
5515
+ throw new CliError('dataset maintenance plan requires --scope.', {
5516
+ code: 'DATASET_MAINTENANCE_SCOPE_REQUIRED',
5517
+ exitCode: 2,
5518
+ });
5519
+ }
5520
+ if (!datasetFlags.operation) {
5521
+ throw new CliError('dataset maintenance plan requires --operation.', {
5522
+ code: 'DATASET_MAINTENANCE_OPERATION_REQUIRED',
5523
+ exitCode: 2,
5524
+ });
5525
+ }
5526
+ if (!datasetFlags.outDir) {
5527
+ throw new CliError('dataset maintenance plan requires --out-dir.', {
5528
+ code: 'DATASET_MAINTENANCE_OUT_DIR_REQUIRED',
5529
+ exitCode: 2,
5530
+ });
5531
+ }
5532
+ const report = await datasetMaintenancePlanImpl({
5533
+ scopePath: datasetFlags.scopePath,
5534
+ operation: datasetFlags.operation,
5535
+ outDir: datasetFlags.outDir,
5536
+ pageSize: datasetFlags.pageSize,
5537
+ timeoutMs: datasetFlags.timeoutMs,
5538
+ env: deps.env,
5539
+ fetchImpl: deps.fetchImpl,
5189
5540
  });
5541
+ return {
5542
+ exitCode: report.status === 'blocked' ? 1 : 0,
5543
+ stdout: stringifyJson(report, datasetFlags.json),
5544
+ stderr: '',
5545
+ };
5190
5546
  }
5191
- return plannedCommand('dataset', `maintenance ${action}`);
5547
+ if (action === 'apply') {
5548
+ const datasetFlags = parseDatasetMaintenanceApplyFlags(commandArgs.slice(1));
5549
+ if (datasetFlags.help) {
5550
+ return {
5551
+ exitCode: 0,
5552
+ stdout: `${renderDatasetMaintenanceApplyHelp()}\n`,
5553
+ stderr: '',
5554
+ };
5555
+ }
5556
+ if (!datasetFlags.planPath) {
5557
+ throw new CliError('dataset maintenance apply requires --plan.', {
5558
+ code: 'DATASET_MAINTENANCE_PLAN_REQUIRED',
5559
+ exitCode: 2,
5560
+ });
5561
+ }
5562
+ if (datasetFlags.commit && datasetFlags.dryRun) {
5563
+ throw new CliError('Cannot pass both --commit and --dry-run.', {
5564
+ code: 'DATASET_MAINTENANCE_APPLY_MODE_CONFLICT',
5565
+ exitCode: 2,
5566
+ });
5567
+ }
5568
+ if (datasetFlags.dryRun) {
5569
+ throw new CliError('dataset maintenance apply is commit-only; use dataset maintenance plan for a dry run.', {
5570
+ code: 'DATASET_MAINTENANCE_APPLY_COMMIT_ONLY',
5571
+ exitCode: 2,
5572
+ });
5573
+ }
5574
+ if (!datasetFlags.commit) {
5575
+ throw new CliError('dataset maintenance apply requires --commit.', {
5576
+ code: 'DATASET_MAINTENANCE_APPLY_COMMIT_REQUIRED',
5577
+ exitCode: 2,
5578
+ });
5579
+ }
5580
+ if (!datasetFlags.approvePlan) {
5581
+ throw new CliError('dataset maintenance apply requires --approve-plan <sha256>.', {
5582
+ code: 'DATASET_MAINTENANCE_APPROVAL_REQUIRED',
5583
+ exitCode: 2,
5584
+ });
5585
+ }
5586
+ if (!datasetFlags.confirm) {
5587
+ throw new CliError('dataset maintenance apply requires --confirm <email>.', {
5588
+ code: 'DATASET_MAINTENANCE_CONFIRM_REQUIRED',
5589
+ exitCode: 2,
5590
+ });
5591
+ }
5592
+ const report = await datasetMaintenanceApplyImpl({
5593
+ planPath: datasetFlags.planPath,
5594
+ commit: datasetFlags.commit,
5595
+ approvePlan: datasetFlags.approvePlan,
5596
+ confirm: datasetFlags.confirm,
5597
+ timeoutMs: datasetFlags.timeoutMs,
5598
+ env: deps.env,
5599
+ fetchImpl: deps.fetchImpl,
5600
+ });
5601
+ return {
5602
+ exitCode: report.status === 'completed_with_failures' ? 1 : 0,
5603
+ stdout: stringifyJson(report, datasetFlags.json),
5604
+ stderr: '',
5605
+ };
5606
+ }
5607
+ if (action === 'verify') {
5608
+ const datasetFlags = parseDatasetMaintenanceVerifyFlags(commandArgs.slice(1));
5609
+ if (datasetFlags.help) {
5610
+ return {
5611
+ exitCode: 0,
5612
+ stdout: `${renderDatasetMaintenanceVerifyHelp()}\n`,
5613
+ stderr: '',
5614
+ };
5615
+ }
5616
+ if (!datasetFlags.planPath) {
5617
+ throw new CliError('dataset maintenance verify requires --plan.', {
5618
+ code: 'DATASET_MAINTENANCE_PLAN_REQUIRED',
5619
+ exitCode: 2,
5620
+ });
5621
+ }
5622
+ const report = await datasetMaintenanceVerifyImpl({
5623
+ planPath: datasetFlags.planPath,
5624
+ outDir: datasetFlags.outDir,
5625
+ pageSize: datasetFlags.pageSize,
5626
+ timeoutMs: datasetFlags.timeoutMs,
5627
+ env: deps.env,
5628
+ fetchImpl: deps.fetchImpl,
5629
+ });
5630
+ return {
5631
+ exitCode: report.status === 'failed' ? 1 : 0,
5632
+ stdout: stringifyJson(report, datasetFlags.json),
5633
+ stderr: '',
5634
+ };
5635
+ }
5636
+ throw new CliError("dataset maintenance action must be 'clear-account', 'plan', 'apply', or 'verify'.", {
5637
+ code: 'DATASET_MAINTENANCE_ACTION_INVALID',
5638
+ exitCode: 2,
5639
+ });
5192
5640
  }
5193
5641
  if (command === 'lifecyclemodel' && !subcommand) {
5194
5642
  return { exitCode: 0, stdout: `${renderLifecyclemodelHelp()}\n`, stderr: '' };