dataiku-sdk 0.6.1 → 0.7.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/README.md +85 -0
- package/bin/dss.js +39 -13
- package/dist/packages/types/src/index.d.ts +21 -0
- package/dist/packages/types/src/index.js +7 -0
- package/dist/src/cli.js +1237 -505
- package/dist/src/client.d.ts +18 -17
- package/dist/src/client.js +49 -36
- package/dist/src/config.d.ts +0 -2
- package/dist/src/config.js +1 -16
- package/dist/src/errors.d.ts +2 -1
- package/dist/src/errors.js +3 -1
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.js +2 -2
- package/dist/src/resources/datasets.d.ts +21 -7
- package/dist/src/resources/datasets.js +85 -70
- package/dist/src/resources/flow-zones.d.ts +1 -1
- package/dist/src/resources/flow-zones.js +3 -1
- package/dist/src/resources/jobs.d.ts +1 -0
- package/dist/src/resources/jobs.js +1 -1
- package/dist/src/resources/recipes.d.ts +1 -0
- package/dist/src/resources/recipes.js +42 -2
- package/dist/src/resources/scenarios.d.ts +24 -0
- package/dist/src/resources/scenarios.js +161 -0
- package/dist/src/schemas.d.ts +2 -2
- package/dist/src/schemas.js +1 -1
- package/dist/src/skill.d.ts +5 -0
- package/dist/src/skill.js +93 -100
- package/dist/src/utils/cleanup-ledger.js +22 -1
- package/package.json +2 -1
- package/packages/types/dist/index.d.ts +21 -0
- package/packages/types/dist/index.js +7 -0
package/dist/src/cli.js
CHANGED
|
@@ -3,17 +3,15 @@ import { createHash, } from "node:crypto";
|
|
|
3
3
|
import { readFileSync, } from "node:fs";
|
|
4
4
|
import { mkdir, writeFile, } from "node:fs/promises";
|
|
5
5
|
import { dirname, join, resolve, } from "node:path";
|
|
6
|
-
import { createInterface, } from "node:readline";
|
|
7
|
-
import { Writable, } from "node:stream";
|
|
8
6
|
import { fileURLToPath, } from "node:url";
|
|
9
7
|
import { validateCredentials, } from "./auth.js";
|
|
10
8
|
import { DataikuClient, } from "./client.js";
|
|
11
|
-
import {
|
|
9
|
+
import { getCredentialsPath, loadCredentials, saveCredentials, } from "./config.js";
|
|
12
10
|
import { DataikuError, dataikuErrorCode, } from "./errors.js";
|
|
13
11
|
import { buildDatasetCloneSettings, } from "./resources/datasets.js";
|
|
14
12
|
import { parseJobLogProgress, } from "./resources/jobs.js";
|
|
15
13
|
import { scenarioUpdatePreview, } from "./resources/scenarios.js";
|
|
16
|
-
import { AGENTS, detectAgents, findWorkspaceRoot, installSkill, } from "./skill.js";
|
|
14
|
+
import { AGENTS, detectAgents, findWorkspaceRoot, installSkill, planSkillInstalls, } from "./skill.js";
|
|
17
15
|
import { appendCleanupLedgerEntry, readCleanupLedger, } from "./utils/cleanup-ledger.js";
|
|
18
16
|
import { deepMerge, } from "./utils/deep-merge.js";
|
|
19
17
|
import { sanitizeFileName, } from "./utils/sanitize.js";
|
|
@@ -73,10 +71,10 @@ function gitRevision(packageRoot) {
|
|
|
73
71
|
}
|
|
74
72
|
const PACKAGE_ROOT = findPackageRoot();
|
|
75
73
|
const CLI_VERSION = packageVersion(PACKAGE_ROOT);
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
return
|
|
79
|
-
}
|
|
74
|
+
const CLI_GIT_REVISION = gitRevision(PACKAGE_ROOT);
|
|
75
|
+
function cliVersionResult() {
|
|
76
|
+
return { version: CLI_VERSION, gitRevision: CLI_GIT_REVISION ?? null, };
|
|
77
|
+
}
|
|
80
78
|
function num(v) {
|
|
81
79
|
if (typeof v !== "string")
|
|
82
80
|
return undefined;
|
|
@@ -340,6 +338,336 @@ function flowZoneDetailSummary(zone) {
|
|
|
340
338
|
items: flowZoneItems(zone),
|
|
341
339
|
};
|
|
342
340
|
}
|
|
341
|
+
function optionalStringField(record, keys) {
|
|
342
|
+
for (const key of keys) {
|
|
343
|
+
const value = record[key];
|
|
344
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
345
|
+
return value.trim();
|
|
346
|
+
}
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
function requiredStringArray(value, source) {
|
|
350
|
+
if (!Array.isArray(value)) {
|
|
351
|
+
throw new UsageError(`${source} must be an array of strings.`, "validation_failed");
|
|
352
|
+
}
|
|
353
|
+
return value.map((item, index) => {
|
|
354
|
+
if (typeof item !== "string" || item.trim().length === 0) {
|
|
355
|
+
throw new UsageError(`${source}[${index}] must be a non-empty string.`, "validation_failed");
|
|
356
|
+
}
|
|
357
|
+
return item.trim();
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
function finiteNumberField(record, key, source) {
|
|
361
|
+
const value = record[key];
|
|
362
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
363
|
+
throw new UsageError(`${source}.${key} must be a finite number.`, "validation_failed");
|
|
364
|
+
}
|
|
365
|
+
return value;
|
|
366
|
+
}
|
|
367
|
+
function flowZonePlanColor(value, source) {
|
|
368
|
+
if (value === undefined)
|
|
369
|
+
return undefined;
|
|
370
|
+
if (typeof value !== "string" || !/^#[0-9a-fA-F]{6}$/.test(value.trim())) {
|
|
371
|
+
throw new UsageError(`${source} must be a hex color like #2ab1ac.`, "validation_failed");
|
|
372
|
+
}
|
|
373
|
+
return value.trim();
|
|
374
|
+
}
|
|
375
|
+
function flowZonePlanPosition(value, source) {
|
|
376
|
+
if (value === undefined)
|
|
377
|
+
return undefined;
|
|
378
|
+
const record = plainRecord(value);
|
|
379
|
+
if (!record) {
|
|
380
|
+
throw new UsageError(`${source} must be an object with x and y.`, "validation_failed");
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
x: finiteNumberField(record, "x", source),
|
|
384
|
+
y: finiteNumberField(record, "y", source),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function flowZoneCurrentPosition(zone) {
|
|
388
|
+
const record = zone;
|
|
389
|
+
const position = plainRecord(record.position);
|
|
390
|
+
if (!position)
|
|
391
|
+
return undefined;
|
|
392
|
+
const x = position.x;
|
|
393
|
+
const y = position.y;
|
|
394
|
+
return typeof x === "number" && Number.isFinite(x) && typeof y === "number" && Number.isFinite(y)
|
|
395
|
+
? { x, y, }
|
|
396
|
+
: undefined;
|
|
397
|
+
}
|
|
398
|
+
function flowZoneSamePosition(a, b) {
|
|
399
|
+
if (a === undefined || b === undefined)
|
|
400
|
+
return a === b;
|
|
401
|
+
return a.x === b.x && a.y === b.y;
|
|
402
|
+
}
|
|
403
|
+
function parseFlowZonePlanItem(value, source) {
|
|
404
|
+
if (typeof value === "string")
|
|
405
|
+
return parseFlowZoneObject(value);
|
|
406
|
+
const record = plainRecord(value);
|
|
407
|
+
if (!record) {
|
|
408
|
+
throw new UsageError(`${source} must be TYPE:ID or an object.`, "validation_failed");
|
|
409
|
+
}
|
|
410
|
+
const object = optionalStringField(record, ["object",]);
|
|
411
|
+
if (object)
|
|
412
|
+
return parseFlowZoneObject(object);
|
|
413
|
+
const objectType = optionalStringField(record, ["objectType", "type",]);
|
|
414
|
+
const objectId = optionalStringField(record, ["objectId", "id", "name",]);
|
|
415
|
+
if (!objectType || !objectId) {
|
|
416
|
+
throw new UsageError(`${source} must include objectType/type and objectId/id, or object as TYPE:ID.`, "validation_failed");
|
|
417
|
+
}
|
|
418
|
+
const projectKey = optionalStringField(record, ["projectKey", "project",]);
|
|
419
|
+
return {
|
|
420
|
+
objectType: flowZoneObjectType(objectType),
|
|
421
|
+
objectId,
|
|
422
|
+
...(projectKey ? { projectKey, } : {}),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function addFlowZonePlanTypedItems(items, record, key, objectType, source) {
|
|
426
|
+
if (record[key] === undefined)
|
|
427
|
+
return;
|
|
428
|
+
for (const objectId of requiredStringArray(record[key], `${source}.${key}`)) {
|
|
429
|
+
items.push({ objectType, objectId, });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function flowZoneItemKey(item) {
|
|
433
|
+
return `${item.projectKey ?? ""}\0${item.objectType}\0${item.objectId}`;
|
|
434
|
+
}
|
|
435
|
+
function flowZonePlanLabel(plan) {
|
|
436
|
+
return plan.id ?? plan.name ?? "<unknown>";
|
|
437
|
+
}
|
|
438
|
+
function dedupeFlowZonePlanItems(items) {
|
|
439
|
+
const seen = new Set();
|
|
440
|
+
const result = [];
|
|
441
|
+
for (const item of items) {
|
|
442
|
+
const key = flowZoneItemKey(item);
|
|
443
|
+
if (seen.has(key))
|
|
444
|
+
continue;
|
|
445
|
+
seen.add(key);
|
|
446
|
+
result.push(item);
|
|
447
|
+
}
|
|
448
|
+
return result;
|
|
449
|
+
}
|
|
450
|
+
function flowZonePlanItemKeys(plan) {
|
|
451
|
+
const keys = new Set();
|
|
452
|
+
for (const zone of plan.zones) {
|
|
453
|
+
for (const item of zone.items)
|
|
454
|
+
keys.add(flowZoneItemKey(item));
|
|
455
|
+
}
|
|
456
|
+
return keys;
|
|
457
|
+
}
|
|
458
|
+
function validateUniqueFlowZoneAssignments(plan) {
|
|
459
|
+
const seen = new Map();
|
|
460
|
+
for (const zone of plan.zones) {
|
|
461
|
+
const label = flowZonePlanLabel(zone);
|
|
462
|
+
for (const item of zone.items) {
|
|
463
|
+
const key = flowZoneItemKey(item);
|
|
464
|
+
const previous = seen.get(key);
|
|
465
|
+
if (previous) {
|
|
466
|
+
throw new UsageError(`Flow object ${item.objectType}:${item.objectId} is assigned to both "${previous}" and "${label}".`, "validation_failed");
|
|
467
|
+
}
|
|
468
|
+
seen.set(key, label);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function parseFlowZoneOrganizePlan(input) {
|
|
473
|
+
const zones = input.zones;
|
|
474
|
+
if (!Array.isArray(zones) || zones.length === 0) {
|
|
475
|
+
throw new UsageError("Flow zone organize plan must include a non-empty zones array.", "validation_failed");
|
|
476
|
+
}
|
|
477
|
+
const plan = {
|
|
478
|
+
zones: zones.map((value, index) => {
|
|
479
|
+
const source = `zones[${index}]`;
|
|
480
|
+
const record = plainRecord(value);
|
|
481
|
+
if (!record)
|
|
482
|
+
throw new UsageError(`${source} must be an object.`, "validation_failed");
|
|
483
|
+
const id = optionalStringField(record, ["id", "zoneId",]);
|
|
484
|
+
const name = optionalStringField(record, ["name",]);
|
|
485
|
+
if (!id && !name) {
|
|
486
|
+
throw new UsageError(`${source} must include name or id.`, "validation_failed");
|
|
487
|
+
}
|
|
488
|
+
const items = [];
|
|
489
|
+
const rawItems = record.items ?? record.objects;
|
|
490
|
+
if (rawItems !== undefined) {
|
|
491
|
+
if (!Array.isArray(rawItems)) {
|
|
492
|
+
throw new UsageError(`${source}.items must be an array.`, "validation_failed");
|
|
493
|
+
}
|
|
494
|
+
rawItems.forEach((item, itemIndex) => {
|
|
495
|
+
items.push(parseFlowZonePlanItem(item, `${source}.items[${itemIndex}]`));
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
addFlowZonePlanTypedItems(items, record, "datasets", "DATASET", source);
|
|
499
|
+
addFlowZonePlanTypedItems(items, record, "recipes", "RECIPE", source);
|
|
500
|
+
addFlowZonePlanTypedItems(items, record, "folders", "MANAGED_FOLDER", source);
|
|
501
|
+
addFlowZonePlanTypedItems(items, record, "savedModels", "SAVED_MODEL", source);
|
|
502
|
+
addFlowZonePlanTypedItems(items, record, "modelEvaluationStores", "MODEL_EVALUATION_STORE", source);
|
|
503
|
+
addFlowZonePlanTypedItems(items, record, "streamingEndpoints", "STREAMING_ENDPOINT", source);
|
|
504
|
+
addFlowZonePlanTypedItems(items, record, "labelingTasks", "LABELING_TASK", source);
|
|
505
|
+
addFlowZonePlanTypedItems(items, record, "knowledgeBanks", "RETRIEVABLE_KNOWLEDGE", source);
|
|
506
|
+
return {
|
|
507
|
+
...(id ? { id, } : {}),
|
|
508
|
+
...(name ? { name, } : {}),
|
|
509
|
+
...(record.color !== undefined
|
|
510
|
+
? { color: flowZonePlanColor(record.color, `${source}.color`), }
|
|
511
|
+
: {}),
|
|
512
|
+
...(record.position !== undefined
|
|
513
|
+
? { position: flowZonePlanPosition(record.position, `${source}.position`), }
|
|
514
|
+
: {}),
|
|
515
|
+
items: dedupeFlowZonePlanItems(items),
|
|
516
|
+
};
|
|
517
|
+
}),
|
|
518
|
+
};
|
|
519
|
+
validateUniqueFlowZoneAssignments(plan);
|
|
520
|
+
return plan;
|
|
521
|
+
}
|
|
522
|
+
function readFlowZoneOrganizePlan(flags, usage) {
|
|
523
|
+
const data = typeof flags["file"] === "string"
|
|
524
|
+
? parseJsonObject(readFileSync(flags["file"], "utf-8"), flags["file"])
|
|
525
|
+
: jsonInput(flags);
|
|
526
|
+
if (!data) {
|
|
527
|
+
throw new UsageError(`--data, --data-file, --file, or --stdin is required. Usage: ${usage}`, "missing_required_flag");
|
|
528
|
+
}
|
|
529
|
+
return parseFlowZoneOrganizePlan(data);
|
|
530
|
+
}
|
|
531
|
+
function findFlowZoneForPlan(zones, plan) {
|
|
532
|
+
if (plan.id) {
|
|
533
|
+
const byId = zones.find((zone) => zone.id === plan.id);
|
|
534
|
+
if (byId)
|
|
535
|
+
return byId;
|
|
536
|
+
}
|
|
537
|
+
if (!plan.name)
|
|
538
|
+
return undefined;
|
|
539
|
+
const byName = zones.filter((zone) => zone.name === plan.name);
|
|
540
|
+
if (byName.length > 1) {
|
|
541
|
+
throw new UsageError(`Multiple flow zones named "${plan.name}" exist; use id.`, "validation_failed");
|
|
542
|
+
}
|
|
543
|
+
return byName[0];
|
|
544
|
+
}
|
|
545
|
+
function ensureFlowZonePlanTarget(plan, existing) {
|
|
546
|
+
if (existing || plan.name)
|
|
547
|
+
return;
|
|
548
|
+
throw new UsageError(`Flow zone ${plan.id ?? "<unknown>"} was not found and cannot be created without name.`, "validation_failed");
|
|
549
|
+
}
|
|
550
|
+
function flowZoneExplicitItems(zone) {
|
|
551
|
+
return (zone.items ?? []).map((item) => ({
|
|
552
|
+
objectId: item.objectId,
|
|
553
|
+
objectType: item.objectType,
|
|
554
|
+
...(item.projectKey ? { projectKey: item.projectKey, } : {}),
|
|
555
|
+
}));
|
|
556
|
+
}
|
|
557
|
+
function flowZonePruneItems(existing, plannedItemKeys) {
|
|
558
|
+
if (!existing)
|
|
559
|
+
return [];
|
|
560
|
+
return flowZoneExplicitItems(existing).filter((item) => !plannedItemKeys.has(flowZoneItemKey(item)));
|
|
561
|
+
}
|
|
562
|
+
function flowZoneOrganizeStep(plan, existing, sync, plannedItemKeys) {
|
|
563
|
+
ensureFlowZonePlanTarget(plan, existing);
|
|
564
|
+
const update = {};
|
|
565
|
+
if (existing && plan.name && plan.name !== existing.name)
|
|
566
|
+
update.name = plan.name;
|
|
567
|
+
if (existing && plan.color && plan.color !== existing.color)
|
|
568
|
+
update.color = plan.color;
|
|
569
|
+
if (existing && plan.position !== undefined
|
|
570
|
+
&& !flowZoneSamePosition(flowZoneCurrentPosition(existing), plan.position)) {
|
|
571
|
+
update.position = plan.position;
|
|
572
|
+
}
|
|
573
|
+
const pruneItems = sync ? flowZonePruneItems(existing, plannedItemKeys) : [];
|
|
574
|
+
return {
|
|
575
|
+
target: {
|
|
576
|
+
...(plan.id ? { id: plan.id, } : {}),
|
|
577
|
+
...(plan.name ? { name: plan.name, } : {}),
|
|
578
|
+
...(plan.color ? { color: plan.color, } : {}),
|
|
579
|
+
...(plan.position ? { position: plan.position, } : {}),
|
|
580
|
+
},
|
|
581
|
+
...(existing ? { existing: flowZoneSummary(existing), } : { create: true, }),
|
|
582
|
+
...(Object.keys(update).length > 0 ? { update, } : {}),
|
|
583
|
+
moveItems: plan.items,
|
|
584
|
+
...(pruneItems.length > 0 ? { pruneItems, } : {}),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function flowZoneValidationBucket(index, objectType) {
|
|
588
|
+
switch (objectType) {
|
|
589
|
+
case "DATASET":
|
|
590
|
+
return index.datasets;
|
|
591
|
+
case "RECIPE":
|
|
592
|
+
return index.recipes;
|
|
593
|
+
case "MANAGED_FOLDER":
|
|
594
|
+
return index.folders;
|
|
595
|
+
case "SAVED_MODEL":
|
|
596
|
+
case "MODEL_EVALUATION_STORE":
|
|
597
|
+
case "STREAMING_ENDPOINT":
|
|
598
|
+
case "LABELING_TASK":
|
|
599
|
+
case "RETRIEVABLE_KNOWLEDGE":
|
|
600
|
+
return index.all;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
async function flowZoneValidationIndex(client, projectKey) {
|
|
604
|
+
const result = await client.projects.map({
|
|
605
|
+
projectKey,
|
|
606
|
+
maxNodes: 100_000,
|
|
607
|
+
maxEdges: 100_000,
|
|
608
|
+
});
|
|
609
|
+
const index = {
|
|
610
|
+
projectKey: result.map.projectKey,
|
|
611
|
+
all: new Set(),
|
|
612
|
+
datasets: new Set(),
|
|
613
|
+
recipes: new Set(),
|
|
614
|
+
folders: new Set(),
|
|
615
|
+
};
|
|
616
|
+
for (const node of result.map.nodes) {
|
|
617
|
+
index.all.add(node.id);
|
|
618
|
+
switch (node.kind) {
|
|
619
|
+
case "dataset":
|
|
620
|
+
index.datasets.add(node.id);
|
|
621
|
+
break;
|
|
622
|
+
case "recipe":
|
|
623
|
+
index.recipes.add(node.id);
|
|
624
|
+
break;
|
|
625
|
+
case "folder":
|
|
626
|
+
index.folders.add(node.id);
|
|
627
|
+
break;
|
|
628
|
+
case "other":
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return index;
|
|
633
|
+
}
|
|
634
|
+
async function validateFlowZoneOrganizeObjects(client, plan, projectKey) {
|
|
635
|
+
const indexes = new Map();
|
|
636
|
+
const missing = [];
|
|
637
|
+
const getIndex = async (itemProjectKey) => {
|
|
638
|
+
const requestedProjectKey = itemProjectKey ?? projectKey;
|
|
639
|
+
const cacheKey = requestedProjectKey ?? "";
|
|
640
|
+
const cached = indexes.get(cacheKey);
|
|
641
|
+
if (cached)
|
|
642
|
+
return cached;
|
|
643
|
+
const index = await flowZoneValidationIndex(client, requestedProjectKey);
|
|
644
|
+
indexes.set(cacheKey, index);
|
|
645
|
+
return index;
|
|
646
|
+
};
|
|
647
|
+
for (const zone of plan.zones) {
|
|
648
|
+
for (const item of zone.items) {
|
|
649
|
+
const index = await getIndex(item.projectKey);
|
|
650
|
+
const bucket = flowZoneValidationBucket(index, item.objectType);
|
|
651
|
+
if (bucket.has(item.objectId))
|
|
652
|
+
continue;
|
|
653
|
+
missing.push({
|
|
654
|
+
zone: flowZonePlanLabel(zone),
|
|
655
|
+
objectId: item.objectId,
|
|
656
|
+
objectType: item.objectType,
|
|
657
|
+
...(item.projectKey ? { projectKey: item.projectKey, } : {}),
|
|
658
|
+
reason: `Object not found in project ${item.projectKey ?? index.projectKey}.`,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return { valid: missing.length === 0, missing, };
|
|
663
|
+
}
|
|
664
|
+
function throwFlowZoneValidationError(validation) {
|
|
665
|
+
if (validation.valid)
|
|
666
|
+
return;
|
|
667
|
+
const first = validation.missing[0];
|
|
668
|
+
const suffix = validation.missing.length > 1 ? ` and ${validation.missing.length - 1} more` : "";
|
|
669
|
+
throw new UsageError(`Flow zone organize validation failed: ${first?.objectType}:${first?.objectId} in zone "${first?.zone}" was not found${suffix}.`, "validation_failed");
|
|
670
|
+
}
|
|
343
671
|
async function resolveFlowZoneIdFromFlags(client, flags, projectKey) {
|
|
344
672
|
const zoneId = typeof flags["zone-id"] === "string" ? flags["zone-id"].trim() : "";
|
|
345
673
|
if (zoneId)
|
|
@@ -553,7 +881,24 @@ function json(v, source = "JSON flag") {
|
|
|
553
881
|
return undefined;
|
|
554
882
|
return parseJsonObject(v, source);
|
|
555
883
|
}
|
|
556
|
-
const SQL_QUERY_USAGE = "dss sql query [SQL | --sql QUERY | --sql-file PATH | --sql - | --stdin] (--connection CONN | --dataset FULL_NAME) [--database DB] [--output PATH|--output-file PATH] [--request-timeout MS] [--project-key KEY]";
|
|
884
|
+
const SQL_QUERY_USAGE = "dss sql query [SQL | --sql QUERY | --sql-file PATH | --sql - | --stdin] (--connection CONN | --dataset FULL_NAME) [--database DB] [--output PATH|--output-file PATH] [--preview N] [--request-timeout MS] [--project-key KEY]";
|
|
885
|
+
const DEFAULT_SQL_PREVIEW_ROWS = 5;
|
|
886
|
+
/**
|
|
887
|
+
* Parse `--preview N` into a non-negative row count. Rejects non-integers,
|
|
888
|
+
* negatives, and empty values loudly so a bad flag never silently degrades to a
|
|
889
|
+
* default. `--preview 0` is valid and yields an empty preview (explicit opt-out).
|
|
890
|
+
*/
|
|
891
|
+
function parseSqlPreviewCount(value) {
|
|
892
|
+
if (typeof value !== "string") {
|
|
893
|
+
throw new UsageError(`--preview requires an integer value. Usage: ${SQL_QUERY_USAGE}`, "validation_failed");
|
|
894
|
+
}
|
|
895
|
+
const trimmed = value.trim();
|
|
896
|
+
const parsed = Number(trimmed);
|
|
897
|
+
if (trimmed.length === 0 || !Number.isInteger(parsed) || parsed < 0) {
|
|
898
|
+
throw new UsageError(`--preview must be a non-negative integer (got "${value}"). Usage: ${SQL_QUERY_USAGE}`, "validation_failed");
|
|
899
|
+
}
|
|
900
|
+
return parsed;
|
|
901
|
+
}
|
|
557
902
|
function readStdinText() {
|
|
558
903
|
return readFileSync(0, "utf-8");
|
|
559
904
|
}
|
|
@@ -713,12 +1058,36 @@ function resolveSqlInput(args, flags) {
|
|
|
713
1058
|
if (sources.length > 1) {
|
|
714
1059
|
throw new UsageError(`Choose exactly one SQL input source: --sql, --sql-file, --stdin, or one positional SQL argument. Usage: ${SQL_QUERY_USAGE}`);
|
|
715
1060
|
}
|
|
716
|
-
const query = sources[0].read();
|
|
1061
|
+
const query = stripUtf8Bom(sources[0].read());
|
|
717
1062
|
if (query.trim().length === 0) {
|
|
718
1063
|
throw new UsageError(`SQL input from ${sources[0].label} must not be empty. Usage: ${SQL_QUERY_USAGE}`);
|
|
719
1064
|
}
|
|
720
1065
|
return query;
|
|
721
1066
|
}
|
|
1067
|
+
const CODE_RUN_USAGE = "dss code run (--file PATH | --stdin) [--env ENV] [--timeout MS] [--keep] [--full-log] [--project-key KEY]";
|
|
1068
|
+
function resolveCodeInput(args, flags) {
|
|
1069
|
+
if (args.length > 0) {
|
|
1070
|
+
throw new UsageError(`code run takes no positional arguments; pass the script via --file PATH or --stdin. Usage: ${CODE_RUN_USAGE}`);
|
|
1071
|
+
}
|
|
1072
|
+
const sources = [];
|
|
1073
|
+
if (typeof flags["file"] === "string") {
|
|
1074
|
+
sources.push({ label: "--file", read: () => readFileSync(flags["file"], "utf-8"), });
|
|
1075
|
+
}
|
|
1076
|
+
if (flags["stdin"] === true) {
|
|
1077
|
+
sources.push({ label: "--stdin", read: readStdinText, });
|
|
1078
|
+
}
|
|
1079
|
+
if (sources.length === 0) {
|
|
1080
|
+
throw new UsageError(`Python source is required: pass --file PATH or --stdin. Usage: ${CODE_RUN_USAGE}`);
|
|
1081
|
+
}
|
|
1082
|
+
if (sources.length > 1) {
|
|
1083
|
+
throw new UsageError(`Choose exactly one Python source: --file or --stdin. Usage: ${CODE_RUN_USAGE}`);
|
|
1084
|
+
}
|
|
1085
|
+
const script = stripUtf8Bom(sources[0].read());
|
|
1086
|
+
if (script.trim().length === 0) {
|
|
1087
|
+
throw new UsageError(`Python source from ${sources[0].label} must not be empty. Usage: ${CODE_RUN_USAGE}`);
|
|
1088
|
+
}
|
|
1089
|
+
return script;
|
|
1090
|
+
}
|
|
722
1091
|
async function resolveFolderId(client, nameOrId, flags) {
|
|
723
1092
|
return client.folders.resolveId(nameOrId, flags["project-key"]);
|
|
724
1093
|
}
|
|
@@ -749,8 +1118,40 @@ function formatLineDiff(remoteName, localPath, remoteContent, localContent) {
|
|
|
749
1118
|
}
|
|
750
1119
|
return lines.join("\n");
|
|
751
1120
|
}
|
|
1121
|
+
let outputFieldProjection;
|
|
1122
|
+
function resolveFieldPath(source, field) {
|
|
1123
|
+
let current = source;
|
|
1124
|
+
for (const segment of field.split(".")) {
|
|
1125
|
+
if (current === null || typeof current !== "object" || Array.isArray(current))
|
|
1126
|
+
return null;
|
|
1127
|
+
current = current[segment];
|
|
1128
|
+
}
|
|
1129
|
+
return current ?? null;
|
|
1130
|
+
}
|
|
1131
|
+
function pickResultFields(item, fields) {
|
|
1132
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
1133
|
+
return item;
|
|
1134
|
+
const source = item;
|
|
1135
|
+
const picked = {};
|
|
1136
|
+
for (const field of fields)
|
|
1137
|
+
picked[field] = resolveFieldPath(source, field);
|
|
1138
|
+
return picked;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Project the top-level fields callers asked for via --fields. Arrays are mapped
|
|
1142
|
+
* element-wise; scalars and string results pass through untouched. Requested keys
|
|
1143
|
+
* that are absent become null so every row keeps a stable, predictable shape.
|
|
1144
|
+
*/
|
|
1145
|
+
function projectResultFields(result, fields) {
|
|
1146
|
+
if (Array.isArray(result))
|
|
1147
|
+
return result.map((item) => pickResultFields(item, fields));
|
|
1148
|
+
return pickResultFields(result, fields);
|
|
1149
|
+
}
|
|
752
1150
|
function writeCommandResult(result) {
|
|
753
|
-
|
|
1151
|
+
const projected = outputFieldProjection
|
|
1152
|
+
? projectResultFields(result, outputFieldProjection)
|
|
1153
|
+
: result;
|
|
1154
|
+
process.stdout.write(`${JSON.stringify(projected ?? { ok: true, }, null, 2)}\n`);
|
|
754
1155
|
}
|
|
755
1156
|
function transientBodyWithTargetContext(body, target, elapsedMs) {
|
|
756
1157
|
try {
|
|
@@ -770,7 +1171,7 @@ function transientBodyWithTargetContext(body, target, elapsedMs) {
|
|
|
770
1171
|
}
|
|
771
1172
|
function addTransientTargetContext(error, target, elapsedMs) {
|
|
772
1173
|
if (error instanceof DataikuError && error.category === "transient") {
|
|
773
|
-
throw new DataikuError(error.status, error.statusText, transientBodyWithTargetContext(error.body, target, elapsedMs), error.retry);
|
|
1174
|
+
throw new DataikuError(error.status, error.statusText, transientBodyWithTargetContext(error.body, target, elapsedMs), error.retry, error.requestId);
|
|
774
1175
|
}
|
|
775
1176
|
throw error;
|
|
776
1177
|
}
|
|
@@ -790,6 +1191,27 @@ function commandFailureExitCode(result) {
|
|
|
790
1191
|
return 4;
|
|
791
1192
|
return undefined;
|
|
792
1193
|
}
|
|
1194
|
+
class CommandResultFailure extends Error {
|
|
1195
|
+
result;
|
|
1196
|
+
exitCode;
|
|
1197
|
+
constructor(result, exitCode) {
|
|
1198
|
+
super(commandFailureMessage(result));
|
|
1199
|
+
this.name = "CommandResultFailure";
|
|
1200
|
+
this.result = result;
|
|
1201
|
+
this.exitCode = exitCode;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
function commandFailureMessage(result) {
|
|
1205
|
+
if (isFailedWaitResult(result)) {
|
|
1206
|
+
const record = result;
|
|
1207
|
+
const state = typeof record.state === "string" ? record.state : record.outcome;
|
|
1208
|
+
return `Command completed with failed long-running result${state ? `: ${state}` : ""}.`;
|
|
1209
|
+
}
|
|
1210
|
+
if (result && typeof result === "object" && result.unchanged === false) {
|
|
1211
|
+
return "Command completed with failed assertion result.";
|
|
1212
|
+
}
|
|
1213
|
+
return "Command completed with failed result.";
|
|
1214
|
+
}
|
|
793
1215
|
function isNotFoundError(error) {
|
|
794
1216
|
if (error instanceof DataikuError)
|
|
795
1217
|
return error.category === "not_found";
|
|
@@ -810,6 +1232,22 @@ async function readIfExists(reader) {
|
|
|
810
1232
|
function skipResult(resource, id, reason, extra = {}) {
|
|
811
1233
|
return { skipped: id, reason, resource, ...extra, };
|
|
812
1234
|
}
|
|
1235
|
+
function recipeRoleInputItems(recipe, role) {
|
|
1236
|
+
const inputs = recipe["inputs"];
|
|
1237
|
+
if (!inputs || typeof inputs !== "object")
|
|
1238
|
+
return [];
|
|
1239
|
+
const roleEntry = inputs[role];
|
|
1240
|
+
if (!roleEntry || typeof roleEntry !== "object")
|
|
1241
|
+
return [];
|
|
1242
|
+
const items = roleEntry["items"];
|
|
1243
|
+
return Array.isArray(items) ? items : [];
|
|
1244
|
+
}
|
|
1245
|
+
function recipeInputItemRef(item) {
|
|
1246
|
+
if (!item || typeof item !== "object")
|
|
1247
|
+
return undefined;
|
|
1248
|
+
const ref = item["ref"];
|
|
1249
|
+
return typeof ref === "string" && ref.length > 0 ? ref : undefined;
|
|
1250
|
+
}
|
|
813
1251
|
function planResult(resource, action, options) {
|
|
814
1252
|
return {
|
|
815
1253
|
plan: true,
|
|
@@ -1007,7 +1445,6 @@ function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
|
|
|
1007
1445
|
// Arg parsing
|
|
1008
1446
|
// ---------------------------------------------------------------------------
|
|
1009
1447
|
const BOOLEAN_FLAGS = new Set([
|
|
1010
|
-
"help",
|
|
1011
1448
|
"verbose",
|
|
1012
1449
|
"version",
|
|
1013
1450
|
"stdin",
|
|
@@ -1031,7 +1468,6 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
1031
1468
|
"if-not-exists",
|
|
1032
1469
|
"if-exists",
|
|
1033
1470
|
"json",
|
|
1034
|
-
"report-json",
|
|
1035
1471
|
"no-wait",
|
|
1036
1472
|
"force-rebuild",
|
|
1037
1473
|
"latest",
|
|
@@ -1040,9 +1476,13 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
1040
1476
|
"no-backup",
|
|
1041
1477
|
"payload-only",
|
|
1042
1478
|
"allow-same-path",
|
|
1479
|
+
"sync",
|
|
1480
|
+
"validate-objects",
|
|
1481
|
+
"errors-only",
|
|
1482
|
+
"keep",
|
|
1483
|
+
"full-log",
|
|
1043
1484
|
]);
|
|
1044
1485
|
const SHORT_FLAGS = {
|
|
1045
|
-
h: "help",
|
|
1046
1486
|
v: "verbose",
|
|
1047
1487
|
V: "version",
|
|
1048
1488
|
o: "output",
|
|
@@ -1057,6 +1497,7 @@ const FLAG_ALIASES = {
|
|
|
1057
1497
|
"zone-name": "zone",
|
|
1058
1498
|
};
|
|
1059
1499
|
const VALUE_FLAGS = new Set([
|
|
1500
|
+
"fields",
|
|
1060
1501
|
"activity",
|
|
1061
1502
|
"agent",
|
|
1062
1503
|
"api-key",
|
|
@@ -1080,6 +1521,7 @@ const VALUE_FLAGS = new Set([
|
|
|
1080
1521
|
"database",
|
|
1081
1522
|
"dataset",
|
|
1082
1523
|
"file",
|
|
1524
|
+
"env",
|
|
1083
1525
|
"install-core-packages",
|
|
1084
1526
|
"folder",
|
|
1085
1527
|
"input",
|
|
@@ -1115,6 +1557,7 @@ const VALUE_FLAGS = new Set([
|
|
|
1115
1557
|
"partition",
|
|
1116
1558
|
"parent",
|
|
1117
1559
|
"path",
|
|
1560
|
+
"preview",
|
|
1118
1561
|
"project-key",
|
|
1119
1562
|
"recipe",
|
|
1120
1563
|
"request-timeout",
|
|
@@ -1122,6 +1565,7 @@ const VALUE_FLAGS = new Set([
|
|
|
1122
1565
|
"results-per-page",
|
|
1123
1566
|
"record-cleanup",
|
|
1124
1567
|
"rule-id",
|
|
1568
|
+
"role",
|
|
1125
1569
|
"retries",
|
|
1126
1570
|
"poll-interval",
|
|
1127
1571
|
"python-interpreter",
|
|
@@ -1165,6 +1609,8 @@ const KNOWN_LONG_FLAGS = new Set([
|
|
|
1165
1609
|
...Object.values(FLAG_ALIASES),
|
|
1166
1610
|
]);
|
|
1167
1611
|
function normalizeLongFlag(rawFlagName) {
|
|
1612
|
+
if (rawFlagName === "help")
|
|
1613
|
+
throw unsupportedHelpFlag();
|
|
1168
1614
|
const flagName = FLAG_ALIASES[rawFlagName] ?? rawFlagName;
|
|
1169
1615
|
if (!KNOWN_LONG_FLAGS.has(rawFlagName) && !KNOWN_LONG_FLAGS.has(flagName)) {
|
|
1170
1616
|
throw new UsageError(`Unknown flag: --${rawFlagName}`, "unknown_flag");
|
|
@@ -1231,6 +1677,8 @@ function parseArgs(argv) {
|
|
|
1231
1677
|
}
|
|
1232
1678
|
}
|
|
1233
1679
|
else {
|
|
1680
|
+
if (arg[1] === "h")
|
|
1681
|
+
throw unsupportedHelpFlag();
|
|
1234
1682
|
throw new UsageError(`Unknown flag: -${arg[1]}`, "unknown_flag");
|
|
1235
1683
|
}
|
|
1236
1684
|
}
|
|
@@ -2097,6 +2545,110 @@ const commands = {
|
|
|
2097
2545
|
"dss flow-zone move ZONE_ID --object SAVED_MODEL:model_id",
|
|
2098
2546
|
],
|
|
2099
2547
|
},
|
|
2548
|
+
organize: {
|
|
2549
|
+
handler: async (c, _a, f) => {
|
|
2550
|
+
const usage = "dss flow-zone organize (--data JSON|--data-file PATH|--file PATH|--stdin) [--sync] [--validate-objects] [--dry-run] [--project-key KEY]";
|
|
2551
|
+
const pk = f["project-key"];
|
|
2552
|
+
const plan = readFlowZoneOrganizePlan(f, usage);
|
|
2553
|
+
const sync = f["sync"] === true;
|
|
2554
|
+
const validateObjects = f["validate-objects"] === true;
|
|
2555
|
+
const zones = await c.flowZones.list(pk);
|
|
2556
|
+
const plannedItemKeys = flowZonePlanItemKeys(plan);
|
|
2557
|
+
const planned = plan.zones.map((zonePlan) => flowZoneOrganizeStep(zonePlan, findFlowZoneForPlan(zones, zonePlan), sync, plannedItemKeys));
|
|
2558
|
+
const validation = validateObjects
|
|
2559
|
+
? await validateFlowZoneOrganizeObjects(c, plan, pk)
|
|
2560
|
+
: undefined;
|
|
2561
|
+
if (validation)
|
|
2562
|
+
throwFlowZoneValidationError(validation);
|
|
2563
|
+
const itemCount = plan.zones.reduce((count, zonePlan) => count + zonePlan.items.length, 0);
|
|
2564
|
+
const pruneItemCount = planned.reduce((count, step) => {
|
|
2565
|
+
const pruneItems = Array.isArray(step.pruneItems) ? step.pruneItems : [];
|
|
2566
|
+
return count + pruneItems.length;
|
|
2567
|
+
}, 0);
|
|
2568
|
+
if (f["dry-run"] === true) {
|
|
2569
|
+
return {
|
|
2570
|
+
dryRun: true,
|
|
2571
|
+
action: "organize",
|
|
2572
|
+
resource: "flow-zone",
|
|
2573
|
+
projectKey: pk,
|
|
2574
|
+
sync,
|
|
2575
|
+
validateObjects,
|
|
2576
|
+
zoneCount: plan.zones.length,
|
|
2577
|
+
itemCount,
|
|
2578
|
+
pruneItemCount,
|
|
2579
|
+
...(validation ? { validation, } : {}),
|
|
2580
|
+
planned,
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
const currentZones = [...zones,];
|
|
2584
|
+
const created = [];
|
|
2585
|
+
const updated = [];
|
|
2586
|
+
const moved = [];
|
|
2587
|
+
const pruned = [];
|
|
2588
|
+
for (const zonePlan of plan.zones) {
|
|
2589
|
+
let zone = findFlowZoneForPlan(currentZones, zonePlan);
|
|
2590
|
+
ensureFlowZonePlanTarget(zonePlan, zone);
|
|
2591
|
+
const pruneItems = sync ? flowZonePruneItems(zone, plannedItemKeys) : [];
|
|
2592
|
+
if (!zone) {
|
|
2593
|
+
zone = await c.flowZones.create({
|
|
2594
|
+
name: zonePlan.name,
|
|
2595
|
+
color: zonePlan.color,
|
|
2596
|
+
position: zonePlan.position,
|
|
2597
|
+
projectKey: pk,
|
|
2598
|
+
});
|
|
2599
|
+
currentZones.push(zone);
|
|
2600
|
+
created.push(zone);
|
|
2601
|
+
}
|
|
2602
|
+
else {
|
|
2603
|
+
const patch = {
|
|
2604
|
+
...(zonePlan.name && zonePlan.name !== zone.name ? { name: zonePlan.name, } : {}),
|
|
2605
|
+
...(zonePlan.color && zonePlan.color !== zone.color ? { color: zonePlan.color, } : {}),
|
|
2606
|
+
...(zonePlan.position !== undefined
|
|
2607
|
+
&& !flowZoneSamePosition(flowZoneCurrentPosition(zone), zonePlan.position)
|
|
2608
|
+
? { position: zonePlan.position, }
|
|
2609
|
+
: {}),
|
|
2610
|
+
projectKey: pk,
|
|
2611
|
+
};
|
|
2612
|
+
if (patch.name !== undefined || patch.color !== undefined || patch.position !== undefined) {
|
|
2613
|
+
zone = await c.flowZones.update(zone.id, patch);
|
|
2614
|
+
const index = currentZones.findIndex((candidate) => candidate.id === zone.id);
|
|
2615
|
+
if (index !== -1)
|
|
2616
|
+
currentZones[index] = zone;
|
|
2617
|
+
updated.push(zone);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
if (zonePlan.items.length > 0) {
|
|
2621
|
+
await c.flowZones.moveItems(zone.id, zonePlan.items, pk);
|
|
2622
|
+
moved.push({ zoneId: zone.id, name: zone.name, items: zonePlan.items, });
|
|
2623
|
+
}
|
|
2624
|
+
if (pruneItems.length > 0) {
|
|
2625
|
+
await c.flowZones.moveItems("default", pruneItems, pk);
|
|
2626
|
+
pruned.push({ zoneId: "default", fromZoneId: zone.id, name: zone.name, items: pruneItems, });
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
return {
|
|
2630
|
+
organized: true,
|
|
2631
|
+
action: "organize",
|
|
2632
|
+
resource: "flow-zone",
|
|
2633
|
+
projectKey: pk,
|
|
2634
|
+
sync,
|
|
2635
|
+
validateObjects,
|
|
2636
|
+
zoneCount: plan.zones.length,
|
|
2637
|
+
itemCount,
|
|
2638
|
+
pruneItemCount,
|
|
2639
|
+
created,
|
|
2640
|
+
updated,
|
|
2641
|
+
moved,
|
|
2642
|
+
pruned,
|
|
2643
|
+
};
|
|
2644
|
+
},
|
|
2645
|
+
usage: "dss flow-zone organize (--data JSON|--data-file PATH|--file PATH|--stdin) [--sync] [--validate-objects] [--dry-run] [--project-key KEY]",
|
|
2646
|
+
description: "Create/update flow zones and move objects from a declarative visual organization plan.",
|
|
2647
|
+
examples: [
|
|
2648
|
+
"dss flow-zone organize --file flow-zones.json --dry-run",
|
|
2649
|
+
`dss flow-zone organize --data '{"zones":[{"name":"Raw","color":"#64748b","datasets":["raw_orders"]}]}'`,
|
|
2650
|
+
],
|
|
2651
|
+
},
|
|
2100
2652
|
graph: {
|
|
2101
2653
|
handler: (c, a, f) => {
|
|
2102
2654
|
requireArgs(a, 1, "dss flow-zone graph <id>");
|
|
@@ -2204,10 +2756,11 @@ const commands = {
|
|
|
2204
2756
|
return c.datasets.download(a[0], {
|
|
2205
2757
|
outputPath: f["output"],
|
|
2206
2758
|
projectKey: f["project-key"],
|
|
2759
|
+
limit: num(f["limit"]),
|
|
2207
2760
|
});
|
|
2208
2761
|
},
|
|
2209
|
-
usage: "dss dataset download <name> [--output PATH] [--project-key KEY]",
|
|
2210
|
-
description: "Download
|
|
2762
|
+
usage: "dss dataset download <name> [--output PATH] [--limit N] [--project-key KEY]",
|
|
2763
|
+
description: "Download up to --limit rows (default 100k) as CSV; returns { path, rows, truncated, limit } so truncation is visible.",
|
|
2211
2764
|
examples: ["dss dataset download orders", "dss dataset download orders --output ./data/",],
|
|
2212
2765
|
},
|
|
2213
2766
|
create: {
|
|
@@ -2660,6 +3213,87 @@ const commands = {
|
|
|
2660
3213
|
"cat settings.json | dss recipe update compute_orders --stdin",
|
|
2661
3214
|
],
|
|
2662
3215
|
},
|
|
3216
|
+
"add-input": {
|
|
3217
|
+
handler: async (c, a, f) => {
|
|
3218
|
+
requireArgs(a, 2, "dss recipe add-input <recipe> <dataset> [--role ROLE] [--if-not-exists] [--dry-run] [--project-key KEY]");
|
|
3219
|
+
const role = f["role"] ?? "main";
|
|
3220
|
+
const pk = f["project-key"];
|
|
3221
|
+
const { recipe, } = await c.recipes.get(a[0], { projectKey: pk, });
|
|
3222
|
+
const items = recipeRoleInputItems(recipe, role);
|
|
3223
|
+
const present = items.some((item) => recipeInputItemRef(item) === a[1]);
|
|
3224
|
+
if (present) {
|
|
3225
|
+
if (f["if-not-exists"] === true) {
|
|
3226
|
+
return skipResult("recipe", a[0], "exists", { dataset: a[1], role, });
|
|
3227
|
+
}
|
|
3228
|
+
throw new UsageError(`Dataset "${a[1]}" is already a "${role}" input of recipe "${a[0]}".`, "validation_failed");
|
|
3229
|
+
}
|
|
3230
|
+
const nextItems = [...items, { ref: a[1], deps: [], },];
|
|
3231
|
+
const inputs = nextItems.map(recipeInputItemRef).filter((ref) => Boolean(ref));
|
|
3232
|
+
if (f["dry-run"] === true) {
|
|
3233
|
+
return {
|
|
3234
|
+
dryRun: true,
|
|
3235
|
+
action: "add-input",
|
|
3236
|
+
resource: "recipe",
|
|
3237
|
+
recipe: a[0],
|
|
3238
|
+
dataset: a[1],
|
|
3239
|
+
role,
|
|
3240
|
+
inputs,
|
|
3241
|
+
};
|
|
3242
|
+
}
|
|
3243
|
+
await c.recipes.update(a[0], { recipe: { inputs: { [role]: { items: nextItems, }, }, }, }, pk);
|
|
3244
|
+
return { updated: a[0], resource: "recipe", action: "add-input", role, dataset: a[1], inputs, };
|
|
3245
|
+
},
|
|
3246
|
+
usage: "dss recipe add-input <recipe> <dataset> [--role ROLE] [--if-not-exists] [--dry-run] [--project-key KEY]",
|
|
3247
|
+
description: "Add a dataset as a recipe input by appending one item to the current inputs (no need to resend the whole list).",
|
|
3248
|
+
examples: [
|
|
3249
|
+
"dss recipe add-input compute_orders extra_lookup",
|
|
3250
|
+
"dss recipe add-input compute_orders extra_lookup --if-not-exists --dry-run",
|
|
3251
|
+
],
|
|
3252
|
+
},
|
|
3253
|
+
"remove-input": {
|
|
3254
|
+
handler: async (c, a, f) => {
|
|
3255
|
+
requireArgs(a, 2, "dss recipe remove-input <recipe> <dataset> [--role ROLE] [--if-exists] [--dry-run] [--project-key KEY]");
|
|
3256
|
+
const role = f["role"] ?? "main";
|
|
3257
|
+
const pk = f["project-key"];
|
|
3258
|
+
const { recipe, } = await c.recipes.get(a[0], { projectKey: pk, });
|
|
3259
|
+
const items = recipeRoleInputItems(recipe, role);
|
|
3260
|
+
const present = items.some((item) => recipeInputItemRef(item) === a[1]);
|
|
3261
|
+
if (!present) {
|
|
3262
|
+
if (f["if-exists"] === true) {
|
|
3263
|
+
return skipResult("recipe", a[0], "missing", { dataset: a[1], role, });
|
|
3264
|
+
}
|
|
3265
|
+
throw new UsageError(`Dataset "${a[1]}" is not a "${role}" input of recipe "${a[0]}".`, "validation_failed");
|
|
3266
|
+
}
|
|
3267
|
+
const nextItems = items.filter((item) => recipeInputItemRef(item) !== a[1]);
|
|
3268
|
+
const inputs = nextItems.map(recipeInputItemRef).filter((ref) => Boolean(ref));
|
|
3269
|
+
if (f["dry-run"] === true) {
|
|
3270
|
+
return {
|
|
3271
|
+
dryRun: true,
|
|
3272
|
+
action: "remove-input",
|
|
3273
|
+
resource: "recipe",
|
|
3274
|
+
recipe: a[0],
|
|
3275
|
+
dataset: a[1],
|
|
3276
|
+
role,
|
|
3277
|
+
inputs,
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
await c.recipes.update(a[0], { recipe: { inputs: { [role]: { items: nextItems, }, }, }, }, pk);
|
|
3281
|
+
return {
|
|
3282
|
+
updated: a[0],
|
|
3283
|
+
resource: "recipe",
|
|
3284
|
+
action: "remove-input",
|
|
3285
|
+
role,
|
|
3286
|
+
dataset: a[1],
|
|
3287
|
+
inputs,
|
|
3288
|
+
};
|
|
3289
|
+
},
|
|
3290
|
+
usage: "dss recipe remove-input <recipe> <dataset> [--role ROLE] [--if-exists] [--dry-run] [--project-key KEY]",
|
|
3291
|
+
description: "Remove a dataset from a recipe's inputs by dropping one item from the current inputs.",
|
|
3292
|
+
examples: [
|
|
3293
|
+
"dss recipe remove-input compute_orders stale_lookup",
|
|
3294
|
+
"dss recipe remove-input compute_orders stale_lookup --if-exists --dry-run",
|
|
3295
|
+
],
|
|
3296
|
+
},
|
|
2663
3297
|
"get-payload": {
|
|
2664
3298
|
handler: async (c, a, f) => {
|
|
2665
3299
|
requireArgs(a, 1, "dss recipe get-payload <name>");
|
|
@@ -2673,7 +3307,7 @@ const commands = {
|
|
|
2673
3307
|
return payload;
|
|
2674
3308
|
},
|
|
2675
3309
|
usage: "dss recipe get-payload <name> [--raw] [--output PATH] [--project-key KEY]",
|
|
2676
|
-
description: "Print the recipe code payload
|
|
3310
|
+
description: "Print the recipe code payload as JSON; use --raw for raw bytes, not JSON.",
|
|
2677
3311
|
examples: [
|
|
2678
3312
|
"dss recipe get-payload compute_orders --raw",
|
|
2679
3313
|
"dss recipe get-payload compute_orders -o code.py",
|
|
@@ -2687,7 +3321,7 @@ const commands = {
|
|
|
2687
3321
|
});
|
|
2688
3322
|
},
|
|
2689
3323
|
usage: "dss recipe cat <name> [--raw] [--project-key KEY]",
|
|
2690
|
-
description: "Print a recipe code payload;
|
|
3324
|
+
description: "Print a recipe code payload as JSON; use --raw for raw bytes, not JSON.",
|
|
2691
3325
|
examples: ["dss recipe cat compute_orders --raw",],
|
|
2692
3326
|
},
|
|
2693
3327
|
"set-payload": {
|
|
@@ -2870,17 +3504,29 @@ const commands = {
|
|
|
2870
3504
|
examples: ["dss job summary JOB_ID --max-log-lines 200",],
|
|
2871
3505
|
},
|
|
2872
3506
|
log: {
|
|
2873
|
-
handler: (c, a, f) => {
|
|
3507
|
+
handler: async (c, a, f) => {
|
|
2874
3508
|
requireArgs(a, 1, "dss job log <id>");
|
|
2875
|
-
|
|
3509
|
+
const logFilter = f["errors-only"] === true
|
|
3510
|
+
? "errors"
|
|
3511
|
+
: jobLogFilterFromFlag(f["log-filter"]);
|
|
3512
|
+
const log = await c.jobs.log(a[0], {
|
|
2876
3513
|
activity: f["activity"],
|
|
2877
3514
|
logId: f["log-id"],
|
|
3515
|
+
logFilter,
|
|
2878
3516
|
maxLogLines: maxLogLinesFromFlags(f),
|
|
2879
3517
|
projectKey: f["project-key"],
|
|
2880
3518
|
});
|
|
3519
|
+
const outputFile = f["output"]
|
|
3520
|
+
?? f["output-file"];
|
|
3521
|
+
if (!outputFile)
|
|
3522
|
+
return log;
|
|
3523
|
+
const outputPath = resolve(outputFile);
|
|
3524
|
+
await mkdir(dirname(outputPath), { recursive: true, });
|
|
3525
|
+
await writeFile(outputPath, log.endsWith("\n") ? log : `${log}\n`, "utf-8");
|
|
3526
|
+
return outputPath;
|
|
2881
3527
|
},
|
|
2882
|
-
usage: "dss job log <id> [--activity ACTIVITY_ID] [--log-id LOG_ID] [--max-lines N|--max-log-lines N] [--project-key KEY]",
|
|
2883
|
-
description: "Get public API job log output. --log-id is accepted for UI parity but DSS API-key auth cannot select browser-only cat-activity-log files.",
|
|
3528
|
+
usage: "dss job log <id> [--activity ACTIVITY_ID] [--log-id LOG_ID] [--log-filter stdout|stderr|user|errors] [--errors-only] [--max-lines N|--max-log-lines N] [--output PATH] [--project-key KEY]",
|
|
3529
|
+
description: "Get public API job log output. Use --errors-only (or --log-filter errors) to surface just error/traceback lines, and --output PATH to write the log to a file (stdout returns the path). --log-id is accepted for UI parity but DSS API-key auth cannot select browser-only cat-activity-log files.",
|
|
2884
3530
|
examples: [
|
|
2885
3531
|
"dss job log JOB_ID",
|
|
2886
3532
|
"dss job log JOB_ID --activity main --max-log-lines 200",
|
|
@@ -3749,12 +4395,21 @@ const commands = {
|
|
|
3749
4395
|
sql: {
|
|
3750
4396
|
query: {
|
|
3751
4397
|
handler: async (c, a, f) => {
|
|
3752
|
-
const query = resolveSqlInput(a, f);
|
|
3753
4398
|
const connection = f["connection"];
|
|
3754
4399
|
const datasetFullName = f["dataset"];
|
|
3755
4400
|
if ((connection ? 1 : 0) + (datasetFullName ? 1 : 0) !== 1) {
|
|
3756
4401
|
throw new UsageError(`Pass exactly one of --connection or --dataset. Usage: ${SQL_QUERY_USAGE}`);
|
|
3757
4402
|
}
|
|
4403
|
+
const outputFile = f["output"]
|
|
4404
|
+
?? f["output-file"];
|
|
4405
|
+
const previewProvided = f["preview"] !== undefined;
|
|
4406
|
+
if (previewProvided && !outputFile) {
|
|
4407
|
+
throw new UsageError(`--preview requires --output or --output-file. Usage: ${SQL_QUERY_USAGE}`, "validation_failed");
|
|
4408
|
+
}
|
|
4409
|
+
const previewCount = previewProvided
|
|
4410
|
+
? parseSqlPreviewCount(f["preview"])
|
|
4411
|
+
: DEFAULT_SQL_PREVIEW_ROWS;
|
|
4412
|
+
const query = resolveSqlInput(a, f);
|
|
3758
4413
|
const result = await c.sql.query({
|
|
3759
4414
|
query,
|
|
3760
4415
|
connection,
|
|
@@ -3762,8 +4417,6 @@ const commands = {
|
|
|
3762
4417
|
database: f["database"],
|
|
3763
4418
|
projectKey: f["project-key"],
|
|
3764
4419
|
});
|
|
3765
|
-
const outputFile = f["output"]
|
|
3766
|
-
?? f["output-file"];
|
|
3767
4420
|
if (!outputFile)
|
|
3768
4421
|
return result;
|
|
3769
4422
|
const outputPath = resolve(outputFile);
|
|
@@ -3774,6 +4427,7 @@ const commands = {
|
|
|
3774
4427
|
schema: result.schema,
|
|
3775
4428
|
columns: result.columns ?? result.schema,
|
|
3776
4429
|
rowCount: result.rows.length,
|
|
4430
|
+
preview: result.rows.slice(0, previewCount),
|
|
3777
4431
|
outputPath,
|
|
3778
4432
|
written: outputPath,
|
|
3779
4433
|
};
|
|
@@ -3785,6 +4439,40 @@ const commands = {
|
|
|
3785
4439
|
"dss sql query --sql-file query.sql --connection my_pg",
|
|
3786
4440
|
"echo 'SELECT 1' | dss sql query --stdin --dataset MYPROJ.orders",
|
|
3787
4441
|
"dss sql query --sql-file query.sql --connection my_pg --output results.json --request-timeout 120000",
|
|
4442
|
+
"dss sql query --sql-file query.sql --connection my_pg --output results.json --preview 10",
|
|
4443
|
+
],
|
|
4444
|
+
},
|
|
4445
|
+
},
|
|
4446
|
+
code: {
|
|
4447
|
+
run: {
|
|
4448
|
+
handler: async (c, a, f) => {
|
|
4449
|
+
const script = resolveCodeInput(a, f);
|
|
4450
|
+
const run = await c.scenarios.runScript(script, {
|
|
4451
|
+
envName: f["env"],
|
|
4452
|
+
projectKey: f["project-key"],
|
|
4453
|
+
timeoutMs: num(f["timeout"]),
|
|
4454
|
+
keepScenario: f["keep"] === true,
|
|
4455
|
+
});
|
|
4456
|
+
const result = {
|
|
4457
|
+
outcome: run.outcome,
|
|
4458
|
+
success: run.success,
|
|
4459
|
+
runId: run.runId,
|
|
4460
|
+
elapsedMs: run.elapsedMs,
|
|
4461
|
+
pollCount: run.pollCount,
|
|
4462
|
+
output: run.output ?? "",
|
|
4463
|
+
};
|
|
4464
|
+
if (f["full-log"] === true || run.output === undefined) {
|
|
4465
|
+
result.log = run.log;
|
|
4466
|
+
}
|
|
4467
|
+
return result;
|
|
4468
|
+
},
|
|
4469
|
+
usage: CODE_RUN_USAGE,
|
|
4470
|
+
description: "Run one-off Python in a DSS code env via a throwaway custom-python scenario; returns the script's captured output (stdout+stderr) plus outcome/success. Pass --full-log for the raw DSS run log. Exits 4 on a non-SUCCESS outcome.",
|
|
4471
|
+
examples: [
|
|
4472
|
+
"dss code run --file inspect.py",
|
|
4473
|
+
"dss code run --file inspect.py --env py39_pandas",
|
|
4474
|
+
"cat snippet.py | dss code run --stdin",
|
|
4475
|
+
"dss code run --file inspect.py --full-log",
|
|
3788
4476
|
],
|
|
3789
4477
|
},
|
|
3790
4478
|
},
|
|
@@ -4016,7 +4704,7 @@ const commands = {
|
|
|
4016
4704
|
},
|
|
4017
4705
|
};
|
|
4018
4706
|
// ---------------------------------------------------------------------------
|
|
4019
|
-
//
|
|
4707
|
+
// Agent-facing command inventory
|
|
4020
4708
|
// ---------------------------------------------------------------------------
|
|
4021
4709
|
const RESOURCE_NAMES = [
|
|
4022
4710
|
...Object.keys(commands),
|
|
@@ -4027,87 +4715,37 @@ const RESOURCE_NAMES = [
|
|
|
4027
4715
|
"install-skill",
|
|
4028
4716
|
]
|
|
4029
4717
|
.sort();
|
|
4030
|
-
function printTopLevelHelp() {
|
|
4031
|
-
const lines = [
|
|
4032
|
-
"Usage: dss <resource> <action> [args...] [--flags]",
|
|
4033
|
-
"",
|
|
4034
|
-
"Global flags:",
|
|
4035
|
-
" -h, --help Show help",
|
|
4036
|
-
" -v, --verbose Log HTTP requests to stderr",
|
|
4037
|
-
" -V, --version Show version",
|
|
4038
|
-
" --json Emit JSON output (default)",
|
|
4039
|
-
" -o, --output PATH Write output to file (recipe get-payload)",
|
|
4040
|
-
" --url URL Dataiku DSS base URL (env: DATAIKU_URL)",
|
|
4041
|
-
" --api-key KEY API key (env: DATAIKU_API_KEY)",
|
|
4042
|
-
" --project-key KEY Default project key (env: DATAIKU_PROJECT_KEY)",
|
|
4043
|
-
" --timeout MS Operation timeout (build-and-wait, run-and-wait, recipe run)",
|
|
4044
|
-
" --request-timeout MS HTTP request timeout in ms (default: 30000)",
|
|
4045
|
-
" --dry-run Preview destructive actions without executing",
|
|
4046
|
-
" --if-not-exists Skip create if resource already exists",
|
|
4047
|
-
" --if-exists Skip delete if resource is already missing",
|
|
4048
|
-
" --insecure Disable TLS certificate verification",
|
|
4049
|
-
" --ca-cert PATH Extra PEM CA bundle (env: NODE_EXTRA_CA_CERTS)",
|
|
4050
|
-
"",
|
|
4051
|
-
"Resources:",
|
|
4052
|
-
...RESOURCE_NAMES.map((r) => ` ${r}`),
|
|
4053
|
-
"",
|
|
4054
|
-
"Quick start:",
|
|
4055
|
-
" dss auth login Save DSS credentials",
|
|
4056
|
-
" dss auth status Verify connection",
|
|
4057
|
-
" dss doctor Run JSON connectivity diagnostics",
|
|
4058
|
-
" dss project list List accessible projects",
|
|
4059
|
-
" dss dataset list List datasets in default project",
|
|
4060
|
-
" dss dataset preview <name> Preview dataset rows as CSV",
|
|
4061
|
-
" dss recipe get-payload <name> Print recipe code to stdout",
|
|
4062
|
-
" dss recipe download-code <name> Download recipe code to a file",
|
|
4063
|
-
" dss job log <id> View job log output",
|
|
4064
|
-
" dss install-skill Install agent skill for coding agents",
|
|
4065
|
-
];
|
|
4066
|
-
process.stderr.write(`${lines.join("\n")}\n`);
|
|
4067
|
-
}
|
|
4068
|
-
function printResourceHelp(resource) {
|
|
4069
|
-
const actions = commands[resource];
|
|
4070
|
-
if (!actions)
|
|
4071
|
-
return;
|
|
4072
|
-
const maxName = Math.max(...Object.keys(actions).map((n) => n.length));
|
|
4073
|
-
const lines = [
|
|
4074
|
-
`Usage: dss ${resource} <action> [args...] [--flags]`,
|
|
4075
|
-
"",
|
|
4076
|
-
"Actions:",
|
|
4077
|
-
...Object.entries(actions).map(([name, meta,]) => ` ${name.padEnd(maxName + 2)}${meta.description ?? meta.usage}`),
|
|
4078
|
-
"",
|
|
4079
|
-
`Run 'dss ${resource} <action> --help' for details and examples.`,
|
|
4080
|
-
];
|
|
4081
|
-
process.stderr.write(`${lines.join("\n")}\n`);
|
|
4082
|
-
}
|
|
4083
|
-
function printActionHelp(resource, action) {
|
|
4084
|
-
const meta = commands[resource]?.[action];
|
|
4085
|
-
if (!meta)
|
|
4086
|
-
return;
|
|
4087
|
-
const lines = [];
|
|
4088
|
-
if (meta.description)
|
|
4089
|
-
lines.push(meta.description, "");
|
|
4090
|
-
lines.push(`Usage: ${meta.usage}`);
|
|
4091
|
-
if (meta.examples && meta.examples.length > 0) {
|
|
4092
|
-
lines.push("", "Examples:");
|
|
4093
|
-
for (const ex of meta.examples)
|
|
4094
|
-
lines.push(` ${ex}`);
|
|
4095
|
-
}
|
|
4096
|
-
process.stderr.write(`${lines.join("\n")}\n`);
|
|
4097
|
-
}
|
|
4098
4718
|
// ---------------------------------------------------------------------------
|
|
4099
4719
|
// Validation
|
|
4100
4720
|
// ---------------------------------------------------------------------------
|
|
4101
4721
|
class UsageError extends Error {
|
|
4102
4722
|
code;
|
|
4103
4723
|
hint;
|
|
4104
|
-
|
|
4724
|
+
details;
|
|
4725
|
+
constructor(message, code = "usage_error", hint, details) {
|
|
4105
4726
|
super(message);
|
|
4106
4727
|
this.name = "UsageError";
|
|
4107
4728
|
this.code = code;
|
|
4108
4729
|
this.hint = hint;
|
|
4730
|
+
this.details = details;
|
|
4109
4731
|
}
|
|
4110
4732
|
}
|
|
4733
|
+
const COMMANDS_RUN_HINT = "Use `dss commands run` for machine-readable command discovery.";
|
|
4734
|
+
function unsupportedHelpFlag() {
|
|
4735
|
+
return new UsageError("Help screens are not supported.", "usage_error", COMMANDS_RUN_HINT, { command: "dss commands run", });
|
|
4736
|
+
}
|
|
4737
|
+
function noCommandError() {
|
|
4738
|
+
return new UsageError("No command provided.", "usage_error", COMMANDS_RUN_HINT, { command: "dss commands run", resources: RESOURCE_NAMES, });
|
|
4739
|
+
}
|
|
4740
|
+
function missingActionError(resource, validActions, usage) {
|
|
4741
|
+
return new UsageError(`Missing action for ${resource}.`, "usage_error", usage ?? COMMANDS_RUN_HINT, { resource, validActions, });
|
|
4742
|
+
}
|
|
4743
|
+
function unknownResourceError(resource) {
|
|
4744
|
+
return new UsageError(`Unknown resource: ${resource}.`, "usage_error", COMMANDS_RUN_HINT, { resource, validResources: RESOURCE_NAMES, });
|
|
4745
|
+
}
|
|
4746
|
+
function unknownActionError(resource, action, validActions) {
|
|
4747
|
+
return new UsageError(`Unknown action: ${resource} ${action ?? ""}`.trim(), "usage_error", COMMANDS_RUN_HINT, { resource, action, validActions, });
|
|
4748
|
+
}
|
|
4111
4749
|
function requireArgs(args, count, usage) {
|
|
4112
4750
|
if (args.length < count) {
|
|
4113
4751
|
throw new UsageError(`Expected ${count} argument(s), got ${args.length}.\nUsage: ${usage}`, "missing_required_arg");
|
|
@@ -4116,12 +4754,18 @@ function requireArgs(args, count, usage) {
|
|
|
4116
4754
|
// ---------------------------------------------------------------------------
|
|
4117
4755
|
// .env auto-loading
|
|
4118
4756
|
// ---------------------------------------------------------------------------
|
|
4757
|
+
function dataikuEnvironmentEnabled() {
|
|
4758
|
+
return process.env.DATAIKU_DISABLE_ENV !== "1";
|
|
4759
|
+
}
|
|
4119
4760
|
function loadEnvFile() {
|
|
4120
|
-
if (
|
|
4761
|
+
if (!dataikuEnvironmentEnabled())
|
|
4121
4762
|
return;
|
|
4763
|
+
// The invocation cwd takes precedence over the CLI install/root directory, so a
|
|
4764
|
+
// project-local .env where `dss` is invoked overrides defaults shipped beside the
|
|
4765
|
+
// CLI. First writer wins below, so cwd must be listed first.
|
|
4122
4766
|
const dirs = [
|
|
4123
|
-
resolve(dirname(fileURLToPath(import.meta.url)), ".."),
|
|
4124
4767
|
process.cwd(),
|
|
4768
|
+
resolve(dirname(fileURLToPath(import.meta.url)), ".."),
|
|
4125
4769
|
];
|
|
4126
4770
|
for (const dir of dirs) {
|
|
4127
4771
|
try {
|
|
@@ -4151,81 +4795,42 @@ const AUTH_ACTIONS = {
|
|
|
4151
4795
|
login: {
|
|
4152
4796
|
handler: async (flags) => {
|
|
4153
4797
|
const tlsSettings = resolveTlsSettings(flags);
|
|
4154
|
-
|
|
4798
|
+
const useEnv = dataikuEnvironmentEnabled();
|
|
4799
|
+
const url = typeof flags["url"] === "string"
|
|
4800
|
+
? flags["url"]
|
|
4801
|
+
: useEnv
|
|
4802
|
+
? process.env.DATAIKU_URL ?? ""
|
|
4803
|
+
: "";
|
|
4804
|
+
const apiKey = typeof flags["api-key"] === "string"
|
|
4805
|
+
? flags["api-key"]
|
|
4806
|
+
: useEnv
|
|
4807
|
+
? process.env.DATAIKU_API_KEY ?? ""
|
|
4808
|
+
: "";
|
|
4809
|
+
const projectKey = typeof flags["project-key"] === "string"
|
|
4810
|
+
? flags["project-key"]
|
|
4811
|
+
: useEnv
|
|
4812
|
+
? process.env.DATAIKU_PROJECT_KEY
|
|
4813
|
+
: undefined;
|
|
4155
4814
|
if (!url || !apiKey) {
|
|
4156
|
-
|
|
4157
|
-
throw new UsageError("Missing --url and/or --api-key. Provide them as flags or run interactively.");
|
|
4158
|
-
}
|
|
4159
|
-
if (!url)
|
|
4160
|
-
url = await promptLine("DSS URL: ");
|
|
4161
|
-
if (!apiKey)
|
|
4162
|
-
apiKey = await promptSecret("API key: ");
|
|
4163
|
-
if (!projectKey)
|
|
4164
|
-
projectKey = (await promptLine("Project key (optional): ")) || undefined;
|
|
4815
|
+
throw new UsageError("Missing --url and/or --api-key for auth login.", "missing_required_flag", "Pass --url and --api-key, or set DATAIKU_URL and DATAIKU_API_KEY.", { requiredFlags: ["url", "api-key",], env: ["DATAIKU_URL", "DATAIKU_API_KEY",], });
|
|
4165
4816
|
}
|
|
4166
|
-
if (!url)
|
|
4167
|
-
throw new UsageError("URL is required.");
|
|
4168
|
-
if (!apiKey)
|
|
4169
|
-
throw new UsageError("API key is required.");
|
|
4170
|
-
process.stderr.write("Validating credentials... ");
|
|
4171
4817
|
const result = await validateCredentials(url, apiKey, tlsSettings);
|
|
4172
4818
|
if (!result.valid) {
|
|
4173
|
-
process.stderr.write("Failed\n");
|
|
4174
4819
|
if (result.dataikuError)
|
|
4175
4820
|
throw result.dataikuError;
|
|
4176
4821
|
throw new DataikuError(0, "Authentication Failed", result.error ?? "Credential validation failed");
|
|
4177
4822
|
}
|
|
4178
|
-
|
|
4823
|
+
const path = getCredentialsPath();
|
|
4179
4824
|
saveCredentials({ url, apiKey, projectKey, ...tlsSettings, });
|
|
4180
|
-
|
|
4825
|
+
return { saved: true, path, };
|
|
4181
4826
|
},
|
|
4182
|
-
usage: "dss auth login
|
|
4183
|
-
description: "
|
|
4827
|
+
usage: "dss auth login --url URL --api-key KEY [--project-key KEY] [--insecure] [--ca-cert PATH]",
|
|
4828
|
+
description: "Validate and save DSS credentials from flags or environment variables.",
|
|
4184
4829
|
examples: [
|
|
4185
4830
|
"dss auth login --url https://dss.example.com --api-key YOUR_KEY",
|
|
4186
4831
|
"dss auth login --url https://dss.example.com --api-key YOUR_KEY --project-key MYPROJ",
|
|
4187
4832
|
],
|
|
4188
|
-
|
|
4189
|
-
status: {
|
|
4190
|
-
handler: async (flags) => {
|
|
4191
|
-
const creds = loadCredentials();
|
|
4192
|
-
if (!creds) {
|
|
4193
|
-
process.stderr.write("No saved credentials. Run: dss auth login\n");
|
|
4194
|
-
process.exit(1);
|
|
4195
|
-
}
|
|
4196
|
-
const tlsSettings = resolveTlsSettings(flags, creds);
|
|
4197
|
-
const lines = [
|
|
4198
|
-
`URL: ${creds.url}`,
|
|
4199
|
-
`API key: ${maskApiKey(creds.apiKey)}`,
|
|
4200
|
-
`Project key: ${creds.projectKey ?? "(not set)"}`,
|
|
4201
|
-
`TLS verify: ${tlsSettings.tlsRejectUnauthorized === false ? "disabled" : "strict"}`,
|
|
4202
|
-
`CA cert: ${tlsSettings.caCertPath ?? "(default trust store)"}`,
|
|
4203
|
-
];
|
|
4204
|
-
for (const line of lines)
|
|
4205
|
-
process.stderr.write(`${line}\n`);
|
|
4206
|
-
const result = await validateCredentials(creds.url, creds.apiKey, tlsSettings);
|
|
4207
|
-
if (result.valid) {
|
|
4208
|
-
process.stderr.write("Connection: valid\n");
|
|
4209
|
-
}
|
|
4210
|
-
else {
|
|
4211
|
-
process.stderr.write(`Connection: failed (${result.error ?? "unknown error"})\n`);
|
|
4212
|
-
process.stderr.write(`Config: ${getCredentialsPath()}\n`);
|
|
4213
|
-
process.exit(1);
|
|
4214
|
-
}
|
|
4215
|
-
process.stderr.write(`Config: ${getCredentialsPath()}\n`);
|
|
4216
|
-
},
|
|
4217
|
-
usage: "dss auth status [--insecure] [--ca-cert PATH]",
|
|
4218
|
-
description: "Show saved credentials and verify the connection.",
|
|
4219
|
-
examples: ["dss auth status",],
|
|
4220
|
-
},
|
|
4221
|
-
logout: {
|
|
4222
|
-
handler: async (_flags) => {
|
|
4223
|
-
deleteCredentials();
|
|
4224
|
-
process.stderr.write("Credentials removed.\n");
|
|
4225
|
-
},
|
|
4226
|
-
usage: "dss auth logout",
|
|
4227
|
-
description: "Remove saved credentials.",
|
|
4228
|
-
examples: ["dss auth logout",],
|
|
4833
|
+
requiredFlags: ["url", "api-key",],
|
|
4229
4834
|
},
|
|
4230
4835
|
};
|
|
4231
4836
|
function errorDetails(error) {
|
|
@@ -4477,7 +5082,7 @@ async function runDoctor(flags) {
|
|
|
4477
5082
|
ok: credentialsOk,
|
|
4478
5083
|
message: credentialsOk
|
|
4479
5084
|
? "Dataiku URL and API key are configured."
|
|
4480
|
-
: "Missing Dataiku URL and/or API key. Set DATAIKU_URL/DATAIKU_API_KEY
|
|
5085
|
+
: "Missing Dataiku URL and/or API key. Set DATAIKU_URL/DATAIKU_API_KEY or pass --url/--api-key.",
|
|
4481
5086
|
});
|
|
4482
5087
|
let accessibleProjects;
|
|
4483
5088
|
if (credentialsOk) {
|
|
@@ -4556,13 +5161,13 @@ async function runDoctor(flags) {
|
|
|
4556
5161
|
async function runFixtures(flags) {
|
|
4557
5162
|
const { url, apiKey, projectKey, tlsRejectUnauthorized, caCertPath, } = resolveCredentials(flags);
|
|
4558
5163
|
if (!url) {
|
|
4559
|
-
throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL
|
|
5164
|
+
throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL or pass --url.", "missing_required_flag");
|
|
4560
5165
|
}
|
|
4561
5166
|
if (!apiKey) {
|
|
4562
|
-
throw new UsageError("Missing API key. Set DATAIKU_API_KEY
|
|
5167
|
+
throw new UsageError("Missing API key. Set DATAIKU_API_KEY or pass --api-key.", "missing_required_flag");
|
|
4563
5168
|
}
|
|
4564
5169
|
if (!projectKey) {
|
|
4565
|
-
throw new UsageError("Missing project key. Set DATAIKU_PROJECT_KEY
|
|
5170
|
+
throw new UsageError("Missing project key. Set DATAIKU_PROJECT_KEY or pass --project-key.", "missing_required_flag");
|
|
4566
5171
|
}
|
|
4567
5172
|
currentCommandContext.projectKey = projectKey;
|
|
4568
5173
|
const requestTimeoutMs = num(flags["request-timeout"]);
|
|
@@ -4636,7 +5241,7 @@ const PROJECT_SCOPED_RESOURCES = new Set([
|
|
|
4636
5241
|
"variable",
|
|
4637
5242
|
"wiki",
|
|
4638
5243
|
]);
|
|
4639
|
-
const GLOBAL_AGENT_FLAGS = ["
|
|
5244
|
+
const GLOBAL_AGENT_FLAGS = ["json", "verbose", "fields",];
|
|
4640
5245
|
const AUTHENTICATED_AGENT_FLAGS = [
|
|
4641
5246
|
"url",
|
|
4642
5247
|
"api-key",
|
|
@@ -4645,9 +5250,12 @@ const AUTHENTICATED_AGENT_FLAGS = [
|
|
|
4645
5250
|
"insecure",
|
|
4646
5251
|
"ca-cert",
|
|
4647
5252
|
];
|
|
4648
|
-
const COMMANDS_USAGE = "dss commands [--json]";
|
|
5253
|
+
const COMMANDS_USAGE = "dss commands run [--json]";
|
|
4649
5254
|
const COMMANDS_DESCRIPTION = "Print the machine-readable command registry for agent planning.";
|
|
4650
|
-
const COMMANDS_EXAMPLES = ["dss commands", "dss commands --json",];
|
|
5255
|
+
const COMMANDS_EXAMPLES = ["dss commands run", "dss commands run --json",];
|
|
5256
|
+
const VERSION_USAGE = "dss version";
|
|
5257
|
+
const VERSION_DESCRIPTION = "Print the CLI version and git revision as JSON.";
|
|
5258
|
+
const VERSION_EXAMPLES = ["dss version", "dss --version",];
|
|
4651
5259
|
const INSTALL_SKILL_USAGE = "dss install-skill [--global] [--agent NAME] [--target PATH] [--list-agents] [--dry-run] [--plan]";
|
|
4652
5260
|
const INSTALL_SKILL_DESCRIPTION = "Install the dataiku-dss agent skill for detected coding agents.";
|
|
4653
5261
|
const INSTALL_SKILL_EXAMPLES = [
|
|
@@ -4666,6 +5274,22 @@ const FIXTURES_EXAMPLES = [
|
|
|
4666
5274
|
"dss fixtures --json",
|
|
4667
5275
|
"dss fixtures --json --allow-types Filesystem,Inline",
|
|
4668
5276
|
];
|
|
5277
|
+
const ALLOWED_CLEANUP_ACTIONS = new Set([
|
|
5278
|
+
// Must mirror every cleanup.argv shape emitted by cleanupLedgerEntry().
|
|
5279
|
+
"dataset delete",
|
|
5280
|
+
"recipe delete",
|
|
5281
|
+
"scenario delete",
|
|
5282
|
+
"flow-zone delete",
|
|
5283
|
+
"wiki delete",
|
|
5284
|
+
"dashboard delete",
|
|
5285
|
+
"insight delete",
|
|
5286
|
+
"data-quality delete-rule",
|
|
5287
|
+
"code-env delete",
|
|
5288
|
+
"folder delete-file",
|
|
5289
|
+
]);
|
|
5290
|
+
function isAllowedCleanupAction(resource, action) {
|
|
5291
|
+
return ALLOWED_CLEANUP_ACTIONS.has(`${resource} ${action}`);
|
|
5292
|
+
}
|
|
4669
5293
|
function uniqueStrings(values) {
|
|
4670
5294
|
return [...new Set(values),];
|
|
4671
5295
|
}
|
|
@@ -4730,26 +5354,33 @@ function extractPositionals(usage) {
|
|
|
4730
5354
|
function inferSideEffect(resource, action) {
|
|
4731
5355
|
if (resource === "auth")
|
|
4732
5356
|
return "auth";
|
|
4733
|
-
if (resource === "doctor" || resource === "commands" || resource === "fixtures"
|
|
5357
|
+
if (resource === "doctor" || resource === "commands" || resource === "fixtures"
|
|
5358
|
+
|| resource === "version") {
|
|
4734
5359
|
return "read";
|
|
5360
|
+
}
|
|
4735
5361
|
if (resource === "install-skill")
|
|
4736
5362
|
return "write";
|
|
4737
5363
|
if (resource === "data-quality" && action === "compute")
|
|
4738
5364
|
return "write";
|
|
4739
5365
|
if (READ_ACTIONS.has(action))
|
|
4740
5366
|
return "read";
|
|
4741
|
-
if (/^(create|clone|restore|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout)/
|
|
5367
|
+
if (/^(create|clone|restore|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout|add|remove)/
|
|
4742
5368
|
.test(action)) {
|
|
4743
5369
|
return "write";
|
|
4744
5370
|
}
|
|
4745
5371
|
return "read";
|
|
4746
5372
|
}
|
|
4747
5373
|
function inferRequiresAuth(resource) {
|
|
4748
|
-
return resource !== "auth"
|
|
5374
|
+
return resource !== "auth"
|
|
5375
|
+
&& resource !== "commands"
|
|
5376
|
+
&& resource !== "install-skill"
|
|
5377
|
+
&& resource !== "version";
|
|
4749
5378
|
}
|
|
4750
5379
|
function inferRequiresProject(resource, action, usage) {
|
|
4751
|
-
if (resource === "
|
|
5380
|
+
if (resource === "auth" || resource === "doctor" || resource === "commands"
|
|
5381
|
+
|| resource === "install-skill" || resource === "version") {
|
|
4752
5382
|
return false;
|
|
5383
|
+
}
|
|
4753
5384
|
if (PROJECT_SCOPED_RESOURCES.has(resource))
|
|
4754
5385
|
return true;
|
|
4755
5386
|
if (resource === "project" && action !== "list")
|
|
@@ -4777,13 +5408,16 @@ const STRING_OUTPUT_ACTIONS = new Set([
|
|
|
4777
5408
|
"cat",
|
|
4778
5409
|
"log",
|
|
4779
5410
|
"log-url",
|
|
4780
|
-
"preview",
|
|
4781
5411
|
]);
|
|
4782
5412
|
function inferOutputShape(resource, action) {
|
|
4783
|
-
if (resource === "auth" || resource === "install-skill"
|
|
4784
|
-
|
|
5413
|
+
if (resource === "auth" || resource === "commands" || resource === "install-skill"
|
|
5414
|
+
|| resource === "version") {
|
|
5415
|
+
return "object";
|
|
5416
|
+
}
|
|
4785
5417
|
if (ARRAY_OUTPUT_ACTIONS.has(action))
|
|
4786
5418
|
return "array";
|
|
5419
|
+
if (resource === "dataset" && action === "download")
|
|
5420
|
+
return "object";
|
|
4787
5421
|
if (STRING_OUTPUT_ACTIONS.has(action))
|
|
4788
5422
|
return "string";
|
|
4789
5423
|
return "object";
|
|
@@ -4798,8 +5432,107 @@ function inferInputContract(usage) {
|
|
|
4798
5432
|
function stripOptionalUsageGroups(usage) {
|
|
4799
5433
|
return usage.replace(/\[[^\]]*\]/g, " ");
|
|
4800
5434
|
}
|
|
4801
|
-
function
|
|
4802
|
-
return
|
|
5435
|
+
function stripAllUsageGroups(usage) {
|
|
5436
|
+
return usage.replace(/\[[^\]]*\]/g, " ").replace(/\([^)]*\)/g, " ");
|
|
5437
|
+
}
|
|
5438
|
+
function topLevelParenGroups(usage) {
|
|
5439
|
+
const groups = [];
|
|
5440
|
+
let depth = 0;
|
|
5441
|
+
let current = "";
|
|
5442
|
+
for (const char of usage) {
|
|
5443
|
+
if (char === "(") {
|
|
5444
|
+
if (depth > 0)
|
|
5445
|
+
current += char;
|
|
5446
|
+
else
|
|
5447
|
+
current = "";
|
|
5448
|
+
depth++;
|
|
5449
|
+
}
|
|
5450
|
+
else if (char === ")") {
|
|
5451
|
+
depth--;
|
|
5452
|
+
if (depth === 0)
|
|
5453
|
+
groups.push(current);
|
|
5454
|
+
else
|
|
5455
|
+
current += char;
|
|
5456
|
+
}
|
|
5457
|
+
else if (depth > 0) {
|
|
5458
|
+
current += char;
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
return groups;
|
|
5462
|
+
}
|
|
5463
|
+
function splitTopLevelChoices(group) {
|
|
5464
|
+
const parts = [];
|
|
5465
|
+
let depth = 0;
|
|
5466
|
+
let current = "";
|
|
5467
|
+
for (const char of group) {
|
|
5468
|
+
if (char === "[" || char === "(")
|
|
5469
|
+
depth++;
|
|
5470
|
+
else if (char === "]" || char === ")")
|
|
5471
|
+
depth--;
|
|
5472
|
+
if (char === "|" && depth === 0) {
|
|
5473
|
+
parts.push(current);
|
|
5474
|
+
current = "";
|
|
5475
|
+
}
|
|
5476
|
+
else {
|
|
5477
|
+
current += char;
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
parts.push(current);
|
|
5481
|
+
return parts;
|
|
5482
|
+
}
|
|
5483
|
+
function flagsInUsageFragment(fragment) {
|
|
5484
|
+
return extractUsageFlags(fragment.replace(/\[[^\]]*\]/g, " "));
|
|
5485
|
+
}
|
|
5486
|
+
/**
|
|
5487
|
+
* Split required usage flags into unconditional flags and mutually-exclusive
|
|
5488
|
+
* choice groups. A required `(--a X | --b Y)` group becomes a requiredOneOf entry
|
|
5489
|
+
* (pick exactly one alternative; an alternative listing several flags must be
|
|
5490
|
+
* supplied together) instead of marking every flag as unconditionally required.
|
|
5491
|
+
*/
|
|
5492
|
+
function deriveRequiredUsage(usage) {
|
|
5493
|
+
const requiredFlags = extractUsageFlags(stripAllUsageGroups(usage));
|
|
5494
|
+
const requiredOneOf = [];
|
|
5495
|
+
for (const group of topLevelParenGroups(usage)) {
|
|
5496
|
+
const alternatives = splitTopLevelChoices(group);
|
|
5497
|
+
if (alternatives.length <= 1) {
|
|
5498
|
+
requiredFlags.push(...flagsInUsageFragment(group));
|
|
5499
|
+
continue;
|
|
5500
|
+
}
|
|
5501
|
+
const oneOf = alternatives
|
|
5502
|
+
.map((alternative) => flagsInUsageFragment(alternative))
|
|
5503
|
+
.filter((alternativeFlags) => alternativeFlags.length > 0);
|
|
5504
|
+
if (oneOf.length > 1)
|
|
5505
|
+
requiredOneOf.push({ oneOf, });
|
|
5506
|
+
else if (oneOf.length === 1)
|
|
5507
|
+
requiredFlags.push(...oneOf[0]);
|
|
5508
|
+
}
|
|
5509
|
+
return { requiredFlags: uniqueStrings(requiredFlags), requiredOneOf, };
|
|
5510
|
+
}
|
|
5511
|
+
const GLOBAL_FLAG_VALUE_HINTS = {
|
|
5512
|
+
url: { valueType: "URL", },
|
|
5513
|
+
fields: { valueType: "CSV", },
|
|
5514
|
+
"api-key": { valueType: "KEY", },
|
|
5515
|
+
"request-timeout": { valueType: "MS", },
|
|
5516
|
+
retries: { valueType: "N", },
|
|
5517
|
+
"ca-cert": { valueType: "PATH", },
|
|
5518
|
+
"project-key": { valueType: "KEY", },
|
|
5519
|
+
"record-cleanup": { valueType: "PATH", },
|
|
5520
|
+
};
|
|
5521
|
+
/** Derive a value placeholder (and enum members) for each value flag from its usage token. */
|
|
5522
|
+
function extractFlagValueHints(usage) {
|
|
5523
|
+
const hints = new Map();
|
|
5524
|
+
for (const match of usage.matchAll(/--([a-z0-9-]+)\s+([a-z]+(?:\|[a-z]+)+)/g)) {
|
|
5525
|
+
const flag = FLAG_ALIASES[match[1]] ?? match[1];
|
|
5526
|
+
if (!hints.has(flag)) {
|
|
5527
|
+
hints.set(flag, { valueType: "enum", enumValues: match[2].split("|"), });
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
for (const match of usage.matchAll(/--([a-z0-9-]+)\s+(<[^>]+>|[A-Z][A-Za-z0-9_]*)/g)) {
|
|
5531
|
+
const flag = FLAG_ALIASES[match[1]] ?? match[1];
|
|
5532
|
+
if (!hints.has(flag))
|
|
5533
|
+
hints.set(flag, { valueType: match[2], });
|
|
5534
|
+
}
|
|
5535
|
+
return hints;
|
|
4803
5536
|
}
|
|
4804
5537
|
function inferPayloadSchema(inputContract) {
|
|
4805
5538
|
if (!inputContract.stdin && !inputContract.dataFlag && !inputContract.dataFileFlag) {
|
|
@@ -4852,6 +5585,8 @@ function inferAsyncKind(resource, action) {
|
|
|
4852
5585
|
}
|
|
4853
5586
|
if (resource === "data-quality" && action === "compute")
|
|
4854
5587
|
return "future";
|
|
5588
|
+
if (resource === "code" && action === "run")
|
|
5589
|
+
return "future";
|
|
4855
5590
|
return "none";
|
|
4856
5591
|
}
|
|
4857
5592
|
function inferIdempotency(sideEffect, action, usage) {
|
|
@@ -4861,6 +5596,8 @@ function inferIdempotency(sideEffect, action, usage) {
|
|
|
4861
5596
|
return "if-not-exists";
|
|
4862
5597
|
if (action.startsWith("delete") && usage.includes("--if-exists"))
|
|
4863
5598
|
return "if-exists";
|
|
5599
|
+
if (/^(clear|refresh|set|save)/.test(action))
|
|
5600
|
+
return "convergent";
|
|
4864
5601
|
return "none";
|
|
4865
5602
|
}
|
|
4866
5603
|
function inferCleanupHint(resource, action) {
|
|
@@ -4891,12 +5628,18 @@ function buildRegistryEntry(resource, action, meta) {
|
|
|
4891
5628
|
...(requiresAuth ? AUTHENTICATED_AGENT_FLAGS : []),
|
|
4892
5629
|
...(requiresProject ? ["project-key",] : []),
|
|
4893
5630
|
]);
|
|
5631
|
+
const derivedRequired = deriveRequiredUsage(meta.usage);
|
|
4894
5632
|
const requiredFlags = meta.requiredFlags
|
|
4895
5633
|
?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.requiredFlags
|
|
4896
|
-
??
|
|
5634
|
+
?? derivedRequired.requiredFlags;
|
|
5635
|
+
const requiredOneOf = meta.requiredOneOf
|
|
5636
|
+
?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.requiredOneOf
|
|
5637
|
+
?? derivedRequired.requiredOneOf;
|
|
5638
|
+
const oneOfFlags = new Set(requiredOneOf.flatMap((choice) => choice.oneOf.flat()));
|
|
4897
5639
|
const optionalFlags = meta.optionalFlags
|
|
4898
5640
|
?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.optionalFlags
|
|
4899
|
-
?? flags.filter((flag) => !requiredFlags.includes(flag));
|
|
5641
|
+
?? flags.filter((flag) => !requiredFlags.includes(flag) && !oneOfFlags.has(flag));
|
|
5642
|
+
const valueHints = extractFlagValueHints(meta.usage);
|
|
4900
5643
|
const inputContract = inferInputContract(meta.usage);
|
|
4901
5644
|
const cleanupHint = inferCleanupHint(resource, action);
|
|
4902
5645
|
const payloadSchema = meta.payloadSchema
|
|
@@ -4913,7 +5656,20 @@ function buildRegistryEntry(resource, action, meta) {
|
|
|
4913
5656
|
usage: meta.usage,
|
|
4914
5657
|
description: meta.description,
|
|
4915
5658
|
examples: meta.examples,
|
|
4916
|
-
flags: flags.map((name) =>
|
|
5659
|
+
flags: flags.map((name) => {
|
|
5660
|
+
const kind = flagKind(name);
|
|
5661
|
+
if (kind === "boolean")
|
|
5662
|
+
return { name, kind, };
|
|
5663
|
+
const hint = valueHints.get(name) ?? GLOBAL_FLAG_VALUE_HINTS[name];
|
|
5664
|
+
if (!hint)
|
|
5665
|
+
return { name, kind, };
|
|
5666
|
+
return {
|
|
5667
|
+
name,
|
|
5668
|
+
kind,
|
|
5669
|
+
valueType: hint.valueType,
|
|
5670
|
+
...(hint.enumValues ? { enumValues: hint.enumValues, } : {}),
|
|
5671
|
+
};
|
|
5672
|
+
}),
|
|
4917
5673
|
positionals: extractPositionals(meta.usage),
|
|
4918
5674
|
sideEffect,
|
|
4919
5675
|
requiresAuth,
|
|
@@ -4929,6 +5685,7 @@ function buildRegistryEntry(resource, action, meta) {
|
|
|
4929
5685
|
dryRun: meta.usage.includes("--dry-run"),
|
|
4930
5686
|
requiredFlags: uniqueStrings(requiredFlags),
|
|
4931
5687
|
optionalFlags: uniqueStrings(optionalFlags),
|
|
5688
|
+
...(requiredOneOf.length > 0 ? { requiredOneOf, } : {}),
|
|
4932
5689
|
...(payloadSchema ? { payloadSchema, } : {}),
|
|
4933
5690
|
...(examplePayload !== undefined ? { examplePayload, } : {}),
|
|
4934
5691
|
...(cleanupCommand ? { cleanupCommand, } : {}),
|
|
@@ -4952,6 +5709,14 @@ function buildCommandRegistry() {
|
|
|
4952
5709
|
examples: COMMANDS_EXAMPLES,
|
|
4953
5710
|
}),
|
|
4954
5711
|
};
|
|
5712
|
+
registry.version = {
|
|
5713
|
+
run: buildRegistryEntry("version", "run", {
|
|
5714
|
+
handler: async () => undefined,
|
|
5715
|
+
usage: VERSION_USAGE,
|
|
5716
|
+
description: VERSION_DESCRIPTION,
|
|
5717
|
+
examples: VERSION_EXAMPLES,
|
|
5718
|
+
}),
|
|
5719
|
+
};
|
|
4955
5720
|
registry["install-skill"] = {
|
|
4956
5721
|
run: buildRegistryEntry("install-skill", "run", {
|
|
4957
5722
|
handler: async () => undefined,
|
|
@@ -4976,6 +5741,16 @@ function buildCommandRegistry() {
|
|
|
4976
5741
|
examples: FIXTURES_EXAMPLES,
|
|
4977
5742
|
}),
|
|
4978
5743
|
};
|
|
5744
|
+
registry.batch = {
|
|
5745
|
+
run: buildRegistryEntry("batch", "run", {
|
|
5746
|
+
handler: async () => undefined,
|
|
5747
|
+
usage: BATCH_USAGE,
|
|
5748
|
+
description: BATCH_DESCRIPTION,
|
|
5749
|
+
examples: BATCH_EXAMPLES,
|
|
5750
|
+
examplePayload: BATCH_EXAMPLE_PAYLOAD,
|
|
5751
|
+
payloadSchema: { stdin: true, dataFlag: true, dataFileFlag: true, jsonShape: "array", },
|
|
5752
|
+
}),
|
|
5753
|
+
};
|
|
4979
5754
|
registry.auth = {};
|
|
4980
5755
|
for (const [action, meta,] of Object.entries(AUTH_ACTIONS)) {
|
|
4981
5756
|
registry.auth[action] = buildRegistryEntry("auth", action, {
|
|
@@ -4983,6 +5758,7 @@ function buildCommandRegistry() {
|
|
|
4983
5758
|
usage: meta.usage,
|
|
4984
5759
|
description: meta.description,
|
|
4985
5760
|
examples: meta.examples,
|
|
5761
|
+
requiredFlags: meta.requiredFlags,
|
|
4986
5762
|
});
|
|
4987
5763
|
}
|
|
4988
5764
|
return registry;
|
|
@@ -5605,10 +6381,10 @@ async function runCleanup(flags) {
|
|
|
5605
6381
|
}
|
|
5606
6382
|
const { url, apiKey, projectKey, tlsRejectUnauthorized, caCertPath, } = resolveCredentials(flags);
|
|
5607
6383
|
if (!url) {
|
|
5608
|
-
throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL
|
|
6384
|
+
throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL or pass --url.", "missing_required_flag");
|
|
5609
6385
|
}
|
|
5610
6386
|
if (!apiKey) {
|
|
5611
|
-
throw new UsageError("Missing API key. Set DATAIKU_API_KEY
|
|
6387
|
+
throw new UsageError("Missing API key. Set DATAIKU_API_KEY or pass --api-key.", "missing_required_flag");
|
|
5612
6388
|
}
|
|
5613
6389
|
const requestTimeoutMs = num(flags["request-timeout"]);
|
|
5614
6390
|
const retryMaxAttempts = num(flags["retries"]);
|
|
@@ -5628,7 +6404,8 @@ async function runCleanup(flags) {
|
|
|
5628
6404
|
try {
|
|
5629
6405
|
const parsed = parseArgs(entry.cleanup.argv);
|
|
5630
6406
|
const [resource, action, ...args] = parsed.positional;
|
|
5631
|
-
if (!resource || !action || !
|
|
6407
|
+
if (!resource || !action || !isAllowedCleanupAction(resource, action)
|
|
6408
|
+
|| !commands[resource]?.[action]) {
|
|
5632
6409
|
throw new UsageError(`Invalid cleanup argv: ${entry.cleanup.argv.join(" ")}`);
|
|
5633
6410
|
}
|
|
5634
6411
|
const result = await commands[resource][action].handler(client, args, parsed.flags);
|
|
@@ -5654,35 +6431,128 @@ async function runCleanup(flags) {
|
|
|
5654
6431
|
exitCode: failures.length > 0 ? 2 : 0,
|
|
5655
6432
|
};
|
|
5656
6433
|
}
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
6434
|
+
const BATCH_USAGE = "dss batch (--data JSON|--data-file PATH|--stdin) [--continue-on-error] [--dry-run]";
|
|
6435
|
+
const BATCH_DESCRIPTION = "Run a sequence of dss commands from a JSON array of argv arrays. Fail-fast by default; returns one envelope with a per-step ok/result/error and exits non-zero if any step failed.";
|
|
6436
|
+
const BATCH_HINT = 'Pass a JSON array of argv arrays, e.g. [["dataset","list"],["recipe","update","r","--data-file","p.json"]].';
|
|
6437
|
+
const BATCH_EXAMPLE_PAYLOAD = [
|
|
6438
|
+
["recipe", "set-payload", "compute_orders", "--file", "code.py", "--no-backup",],
|
|
6439
|
+
["recipe", "update", "compute_orders", "--data-file", "env.json",],
|
|
6440
|
+
["dataset", "update", "orders", "--data-file", "ds.json",],
|
|
6441
|
+
];
|
|
6442
|
+
const BATCH_EXAMPLES = [
|
|
6443
|
+
"dss batch --data-file steps.json",
|
|
6444
|
+
"dss batch --stdin --continue-on-error",
|
|
6445
|
+
];
|
|
6446
|
+
function parseBatchSteps(payload) {
|
|
6447
|
+
if (!Array.isArray(payload)) {
|
|
6448
|
+
throw new UsageError("Batch payload must be a JSON array of command-argument arrays.", "validation_failed", BATCH_HINT, { example: BATCH_EXAMPLE_PAYLOAD, });
|
|
6449
|
+
}
|
|
6450
|
+
return payload.map((step, index) => {
|
|
6451
|
+
if (!Array.isArray(step) || !step.every((token) => typeof token === "string")) {
|
|
6452
|
+
throw new UsageError(`Batch step ${index} must be an array of string arguments.`, "validation_failed", BATCH_HINT);
|
|
6453
|
+
}
|
|
6454
|
+
return step;
|
|
5668
6455
|
});
|
|
5669
6456
|
}
|
|
5670
|
-
function
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
6457
|
+
async function runBatch(flags) {
|
|
6458
|
+
const payload = unknownJsonInput(flags);
|
|
6459
|
+
if (payload === undefined) {
|
|
6460
|
+
throw new UsageError(`Provide steps via --data, --data-file, or --stdin. Usage: ${BATCH_USAGE}`, "missing_required_flag", BATCH_HINT);
|
|
6461
|
+
}
|
|
6462
|
+
const steps = parseBatchSteps(payload);
|
|
6463
|
+
if (flags["dry-run"] === true) {
|
|
6464
|
+
const planned = steps.map((argv, index) => {
|
|
6465
|
+
const { positional, } = parseArgs(argv);
|
|
6466
|
+
const resource = positional[0];
|
|
6467
|
+
const action = positional[1];
|
|
6468
|
+
const runnable = Boolean(resource && action && commands[resource]?.[action]);
|
|
6469
|
+
return { index, args: argv, resource, action, runnable, };
|
|
6470
|
+
});
|
|
6471
|
+
return {
|
|
6472
|
+
result: { dryRun: true, total: steps.length, steps: planned, },
|
|
6473
|
+
exitCode: planned.every((step) => step.runnable) ? 0 : 1,
|
|
6474
|
+
};
|
|
6475
|
+
}
|
|
6476
|
+
const { url, apiKey, projectKey, tlsRejectUnauthorized, caCertPath, } = resolveCredentials(flags);
|
|
6477
|
+
if (!url) {
|
|
6478
|
+
throw new UsageError("Missing Dataiku URL.", "missing_required_flag", "Set DATAIKU_URL or pass --url.", {
|
|
6479
|
+
requiredFlags: ["url",],
|
|
6480
|
+
env: ["DATAIKU_URL",],
|
|
5676
6481
|
});
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
process.stderr.write("\n");
|
|
5683
|
-
res(answer.trim());
|
|
6482
|
+
}
|
|
6483
|
+
if (!apiKey) {
|
|
6484
|
+
throw new UsageError("Missing API key.", "missing_required_flag", "Set DATAIKU_API_KEY or pass --api-key.", {
|
|
6485
|
+
requiredFlags: ["api-key",],
|
|
6486
|
+
env: ["DATAIKU_API_KEY",],
|
|
5684
6487
|
});
|
|
6488
|
+
}
|
|
6489
|
+
const client = new DataikuClient({
|
|
6490
|
+
url,
|
|
6491
|
+
apiKey,
|
|
6492
|
+
projectKey,
|
|
6493
|
+
verbose: flags["verbose"] === true,
|
|
6494
|
+
requestTimeoutMs: num(flags["request-timeout"]),
|
|
6495
|
+
retryMaxAttempts: num(flags["retries"]),
|
|
6496
|
+
tlsRejectUnauthorized,
|
|
6497
|
+
caCertPath,
|
|
5685
6498
|
});
|
|
6499
|
+
const continueOnError = flags["continue-on-error"] === true;
|
|
6500
|
+
const results = [];
|
|
6501
|
+
let firstFailureExit;
|
|
6502
|
+
for (let index = 0; index < steps.length; index++) {
|
|
6503
|
+
const argv = steps[index];
|
|
6504
|
+
const { positional, flags: stepFlags, } = parseArgs(argv);
|
|
6505
|
+
const resource = positional[0];
|
|
6506
|
+
const action = positional[1];
|
|
6507
|
+
if (firstFailureExit !== undefined && !continueOnError) {
|
|
6508
|
+
results.push({ index, args: argv, resource, action, ok: null, skipped: true, });
|
|
6509
|
+
continue;
|
|
6510
|
+
}
|
|
6511
|
+
currentCommandContext = {
|
|
6512
|
+
resource,
|
|
6513
|
+
action,
|
|
6514
|
+
projectKey: typeof stepFlags["project-key"] === "string" ? stepFlags["project-key"] : projectKey,
|
|
6515
|
+
};
|
|
6516
|
+
try {
|
|
6517
|
+
if (!resource)
|
|
6518
|
+
throw noCommandError();
|
|
6519
|
+
const resourceActions = commands[resource];
|
|
6520
|
+
if (!resourceActions)
|
|
6521
|
+
throw unknownResourceError(resource);
|
|
6522
|
+
if (!action) {
|
|
6523
|
+
throw missingActionError(resource, Object.keys(resourceActions), `dss ${resource} <action>`);
|
|
6524
|
+
}
|
|
6525
|
+
const meta = resourceActions[action];
|
|
6526
|
+
if (!meta)
|
|
6527
|
+
throw unknownActionError(resource, action, Object.keys(resourceActions));
|
|
6528
|
+
const result = await meta.handler(client, positional.slice(2), stepFlags);
|
|
6529
|
+
const failureExitCode = commandFailureExitCode(result);
|
|
6530
|
+
if (failureExitCode !== undefined)
|
|
6531
|
+
throw new CommandResultFailure(result, failureExitCode);
|
|
6532
|
+
const stepFieldsFlag = stepFlags["fields"];
|
|
6533
|
+
const stepFields = typeof stepFieldsFlag === "string"
|
|
6534
|
+
? stepFieldsFlag.split(",").map((field) => field.trim()).filter((field) => field.length > 0)
|
|
6535
|
+
: [];
|
|
6536
|
+
const stepResult = stepFields.length > 0 ? projectResultFields(result, stepFields) : result;
|
|
6537
|
+
results.push({ index, args: argv, resource, action, ok: true, result: stepResult, });
|
|
6538
|
+
}
|
|
6539
|
+
catch (error) {
|
|
6540
|
+
const envelope = buildErrorReport(error);
|
|
6541
|
+
results.push({ index, args: argv, resource, action, ok: false, error: envelope, });
|
|
6542
|
+
if (firstFailureExit === undefined)
|
|
6543
|
+
firstFailureExit = envelope.exitCode;
|
|
6544
|
+
}
|
|
6545
|
+
}
|
|
6546
|
+
const ok = firstFailureExit === undefined;
|
|
6547
|
+
return {
|
|
6548
|
+
result: {
|
|
6549
|
+
ok,
|
|
6550
|
+
total: steps.length,
|
|
6551
|
+
completed: results.filter((step) => step.ok !== null).length,
|
|
6552
|
+
steps: results,
|
|
6553
|
+
},
|
|
6554
|
+
exitCode: ok ? 0 : firstFailureExit ?? 2,
|
|
6555
|
+
};
|
|
5686
6556
|
}
|
|
5687
6557
|
// ---------------------------------------------------------------------------
|
|
5688
6558
|
// Credential resolution
|
|
@@ -5695,12 +6565,15 @@ function resolveCredentials(flags) {
|
|
|
5695
6565
|
let apiKey = hasApiKeyFlag ? flags["api-key"] : undefined;
|
|
5696
6566
|
let projectKey = hasProjectKeyFlag ? flags["project-key"] : undefined;
|
|
5697
6567
|
const saved = loadCredentials();
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
6568
|
+
const useEnv = dataikuEnvironmentEnabled();
|
|
6569
|
+
if (useEnv) {
|
|
6570
|
+
if (!hasUrlFlag)
|
|
6571
|
+
url ??= process.env.DATAIKU_URL;
|
|
6572
|
+
if (!hasApiKeyFlag)
|
|
6573
|
+
apiKey ??= process.env.DATAIKU_API_KEY;
|
|
6574
|
+
if (!hasProjectKeyFlag)
|
|
6575
|
+
projectKey ??= process.env.DATAIKU_PROJECT_KEY;
|
|
6576
|
+
}
|
|
5704
6577
|
if (saved) {
|
|
5705
6578
|
if (!hasUrlFlag)
|
|
5706
6579
|
url ??= saved.url;
|
|
@@ -5717,10 +6590,6 @@ function resolveCredentials(flags) {
|
|
|
5717
6590
|
};
|
|
5718
6591
|
}
|
|
5719
6592
|
let currentCommandContext = {};
|
|
5720
|
-
function isReportJsonRequested() {
|
|
5721
|
-
return process.env.DSS_REPORT_JSON === "1"
|
|
5722
|
-
|| process.argv.slice(2).some((arg) => arg === "--report-json" || arg.startsWith("--report-json="));
|
|
5723
|
-
}
|
|
5724
6593
|
function rawFlagValue(argv, flagName) {
|
|
5725
6594
|
const longFlag = `--${flagName}`;
|
|
5726
6595
|
for (let index = 0; index < argv.length; index++) {
|
|
@@ -5734,6 +6603,12 @@ function rawFlagValue(argv, flagName) {
|
|
|
5734
6603
|
}
|
|
5735
6604
|
return undefined;
|
|
5736
6605
|
}
|
|
6606
|
+
function commandIsProjectScoped(resource, action) {
|
|
6607
|
+
if (!resource)
|
|
6608
|
+
return false;
|
|
6609
|
+
const usage = commands[resource]?.[action ?? ""]?.usage ?? "";
|
|
6610
|
+
return inferRequiresProject(resource, action ?? "", usage);
|
|
6611
|
+
}
|
|
5737
6612
|
function rawCommandContext() {
|
|
5738
6613
|
const argv = process.argv.slice(2);
|
|
5739
6614
|
const positionals = [];
|
|
@@ -5758,13 +6633,19 @@ function rawCommandContext() {
|
|
|
5758
6633
|
}
|
|
5759
6634
|
positionals.push(arg);
|
|
5760
6635
|
}
|
|
6636
|
+
const resource = currentCommandContext.resource ?? positionals[0];
|
|
6637
|
+
const action = currentCommandContext.action ?? positionals[1];
|
|
6638
|
+
const explicitProjectKey = rawFlagValue(argv, "project-key") ?? rawFlagValue(argv, "project");
|
|
6639
|
+
const ambientProjectKey = dataikuEnvironmentEnabled()
|
|
6640
|
+
? process.env.DATAIKU_PROJECT_KEY
|
|
6641
|
+
: undefined;
|
|
5761
6642
|
return {
|
|
5762
|
-
resource
|
|
5763
|
-
action
|
|
5764
|
-
projectKey:
|
|
5765
|
-
??
|
|
5766
|
-
|
|
5767
|
-
|
|
6643
|
+
resource,
|
|
6644
|
+
action,
|
|
6645
|
+
projectKey: explicitProjectKey
|
|
6646
|
+
?? (commandIsProjectScoped(resource, action)
|
|
6647
|
+
? currentCommandContext.projectKey ?? ambientProjectKey
|
|
6648
|
+
: undefined),
|
|
5768
6649
|
};
|
|
5769
6650
|
}
|
|
5770
6651
|
function requestIdFromBody(body) {
|
|
@@ -5777,26 +6658,52 @@ function requestIdFromBody(body) {
|
|
|
5777
6658
|
return undefined;
|
|
5778
6659
|
}
|
|
5779
6660
|
}
|
|
6661
|
+
function errorExitCode(err) {
|
|
6662
|
+
if (err instanceof CommandResultFailure)
|
|
6663
|
+
return err.exitCode;
|
|
6664
|
+
if (err instanceof UsageError)
|
|
6665
|
+
return 1;
|
|
6666
|
+
if (err instanceof DataikuError)
|
|
6667
|
+
return err.category === "transient" ? 3 : 2;
|
|
6668
|
+
return 2;
|
|
6669
|
+
}
|
|
5780
6670
|
function buildErrorReport(err) {
|
|
5781
6671
|
const context = rawCommandContext();
|
|
6672
|
+
const exitCode = errorExitCode(err);
|
|
5782
6673
|
if (err instanceof UsageError) {
|
|
5783
6674
|
return {
|
|
6675
|
+
ok: false,
|
|
6676
|
+
error: err.message,
|
|
5784
6677
|
code: err.code,
|
|
5785
6678
|
category: "usage",
|
|
5786
|
-
|
|
6679
|
+
exitCode,
|
|
5787
6680
|
...(err.hint ? { hint: err.hint, } : {}),
|
|
6681
|
+
...(err.details ? { details: err.details, } : {}),
|
|
6682
|
+
...context,
|
|
6683
|
+
};
|
|
6684
|
+
}
|
|
6685
|
+
if (err instanceof CommandResultFailure) {
|
|
6686
|
+
return {
|
|
6687
|
+
ok: false,
|
|
6688
|
+
error: err.message,
|
|
6689
|
+
code: "long_running_failure",
|
|
6690
|
+
category: "dss",
|
|
6691
|
+
exitCode: err.exitCode,
|
|
6692
|
+
details: { result: err.result, },
|
|
5788
6693
|
...context,
|
|
5789
6694
|
};
|
|
5790
6695
|
}
|
|
5791
6696
|
if (err instanceof DataikuError) {
|
|
5792
6697
|
return {
|
|
6698
|
+
ok: false,
|
|
6699
|
+
error: err.message,
|
|
5793
6700
|
code: dataikuErrorCode(err.category),
|
|
5794
6701
|
category: "dss",
|
|
5795
|
-
|
|
6702
|
+
exitCode,
|
|
5796
6703
|
hint: err.retryHint,
|
|
5797
6704
|
status: err.status,
|
|
5798
6705
|
retryable: err.retryable,
|
|
5799
|
-
requestId: requestIdFromBody(err.body),
|
|
6706
|
+
requestId: err.requestId ?? requestIdFromBody(err.body),
|
|
5800
6707
|
details: {
|
|
5801
6708
|
dssCategory: err.category,
|
|
5802
6709
|
statusText: err.statusText,
|
|
@@ -5808,190 +6715,109 @@ function buildErrorReport(err) {
|
|
|
5808
6715
|
}
|
|
5809
6716
|
const message = err instanceof Error ? err.message : String(err);
|
|
5810
6717
|
return {
|
|
6718
|
+
ok: false,
|
|
6719
|
+
error: message,
|
|
5811
6720
|
code: "internal_error",
|
|
5812
6721
|
category: "internal",
|
|
5813
|
-
|
|
6722
|
+
exitCode,
|
|
5814
6723
|
...context,
|
|
5815
6724
|
};
|
|
5816
6725
|
}
|
|
5817
6726
|
function writeErrorReport(err) {
|
|
5818
6727
|
process.stderr.write(`${JSON.stringify(buildErrorReport(err), null, 2)}\n`);
|
|
5819
6728
|
}
|
|
5820
|
-
function commandRegistryEntry(resource, action) {
|
|
5821
|
-
return buildCommandRegistry()[resource]?.[action];
|
|
5822
|
-
}
|
|
5823
|
-
function writeReportHelp(resource, action) {
|
|
5824
|
-
const entry = commandRegistryEntry(resource, action);
|
|
5825
|
-
if (entry) {
|
|
5826
|
-
process.stderr.write(`${JSON.stringify(entry, null, 2)}\n`);
|
|
5827
|
-
return;
|
|
5828
|
-
}
|
|
5829
|
-
process.stderr.write(`${JSON.stringify({
|
|
5830
|
-
code: "usage_error",
|
|
5831
|
-
category: "usage",
|
|
5832
|
-
message: `No registry entry for ${resource} ${action}.`,
|
|
5833
|
-
resource,
|
|
5834
|
-
action,
|
|
5835
|
-
}, null, 2)}\n`);
|
|
5836
|
-
}
|
|
5837
6729
|
// ---------------------------------------------------------------------------
|
|
5838
6730
|
// Main
|
|
5839
6731
|
// ---------------------------------------------------------------------------
|
|
5840
6732
|
async function main() {
|
|
5841
6733
|
loadEnvFile();
|
|
5842
6734
|
const { positional, flags, } = parseArgs(process.argv.slice(2));
|
|
5843
|
-
|
|
5844
|
-
if (
|
|
5845
|
-
|
|
5846
|
-
|
|
6735
|
+
const fieldsFlag = flags["fields"];
|
|
6736
|
+
if (typeof fieldsFlag === "string") {
|
|
6737
|
+
const selected = fieldsFlag.split(",").map((field) => field.trim()).filter((field) => field.length > 0);
|
|
6738
|
+
if (selected.length > 0)
|
|
6739
|
+
outputFieldProjection = selected;
|
|
5847
6740
|
}
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
if (flags["help"])
|
|
5852
|
-
process.exit(0);
|
|
5853
|
-
process.exit(1);
|
|
6741
|
+
if (flags["version"] === true) {
|
|
6742
|
+
writeCommandResult(cliVersionResult());
|
|
6743
|
+
return;
|
|
5854
6744
|
}
|
|
6745
|
+
if (positional.length === 0)
|
|
6746
|
+
throw noCommandError();
|
|
5855
6747
|
const resource = positional[0];
|
|
5856
6748
|
currentCommandContext = {
|
|
5857
6749
|
resource,
|
|
5858
6750
|
action: positional[1],
|
|
5859
6751
|
projectKey: typeof flags["project-key"] === "string"
|
|
5860
6752
|
? flags["project-key"]
|
|
5861
|
-
:
|
|
6753
|
+
: dataikuEnvironmentEnabled()
|
|
6754
|
+
? process.env.DATAIKU_PROJECT_KEY
|
|
6755
|
+
: undefined,
|
|
5862
6756
|
};
|
|
5863
6757
|
if (resource === "doctor") {
|
|
5864
6758
|
const action = positional[1];
|
|
5865
|
-
|
|
5866
|
-
if (flags["report-json"] === true)
|
|
5867
|
-
writeReportHelp("doctor", "run");
|
|
5868
|
-
else
|
|
5869
|
-
printActionHelp("doctor", "run");
|
|
5870
|
-
process.exit(0);
|
|
5871
|
-
}
|
|
6759
|
+
currentCommandContext.action = action ?? "run";
|
|
5872
6760
|
if (action !== undefined && action !== "run") {
|
|
5873
|
-
throw
|
|
6761
|
+
throw unknownActionError("doctor", action, ["run",]);
|
|
5874
6762
|
}
|
|
5875
6763
|
const { result, exitCode, } = await runDoctor(flags);
|
|
5876
6764
|
writeCommandResult(result);
|
|
5877
|
-
|
|
6765
|
+
if (exitCode !== 0)
|
|
6766
|
+
process.exit(exitCode);
|
|
6767
|
+
return;
|
|
5878
6768
|
}
|
|
5879
|
-
// Auth commands — dispatched before client creation
|
|
5880
6769
|
if (resource === "auth") {
|
|
5881
6770
|
const action = positional[1];
|
|
6771
|
+
const validActions = Object.keys(AUTH_ACTIONS);
|
|
5882
6772
|
if (!action) {
|
|
5883
|
-
|
|
5884
|
-
const lines = [
|
|
5885
|
-
"Usage: dss auth <action> [--flags]",
|
|
5886
|
-
"",
|
|
5887
|
-
"Actions:",
|
|
5888
|
-
...Object.entries(AUTH_ACTIONS).map(([name, meta,]) => ` ${name.padEnd(maxName + 2)}${meta.description ?? meta.usage}`),
|
|
5889
|
-
"",
|
|
5890
|
-
"Run 'dss auth <action> --help' for details and examples.",
|
|
5891
|
-
];
|
|
5892
|
-
process.stderr.write(`${lines.join("\n")}\n`);
|
|
5893
|
-
process.exit(flags["help"] === true ? 0 : 1);
|
|
6773
|
+
throw missingActionError("auth", validActions, "dss auth login --url URL --api-key KEY");
|
|
5894
6774
|
}
|
|
6775
|
+
currentCommandContext.action = action;
|
|
5895
6776
|
const authMeta = AUTH_ACTIONS[action];
|
|
5896
|
-
if (!authMeta)
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
process.stderr.write(`Unknown action: auth ${action}\nAvailable: ${Object.keys(AUTH_ACTIONS).join(", ")}\n`);
|
|
5901
|
-
process.exit(1);
|
|
5902
|
-
}
|
|
5903
|
-
if (flags["help"] === true) {
|
|
5904
|
-
if (flags["report-json"] === true) {
|
|
5905
|
-
writeReportHelp("auth", action);
|
|
5906
|
-
}
|
|
5907
|
-
else {
|
|
5908
|
-
const lines = [];
|
|
5909
|
-
if (authMeta.description)
|
|
5910
|
-
lines.push(authMeta.description, "");
|
|
5911
|
-
lines.push(`Usage: ${authMeta.usage}`);
|
|
5912
|
-
if (authMeta.examples && authMeta.examples.length > 0) {
|
|
5913
|
-
lines.push("", "Examples:");
|
|
5914
|
-
for (const ex of authMeta.examples)
|
|
5915
|
-
lines.push(` ${ex}`);
|
|
5916
|
-
}
|
|
5917
|
-
process.stderr.write(`${lines.join("\n")}\n`);
|
|
5918
|
-
}
|
|
5919
|
-
process.exit(0);
|
|
5920
|
-
}
|
|
5921
|
-
await authMeta.handler(flags);
|
|
6777
|
+
if (!authMeta)
|
|
6778
|
+
throw unknownActionError("auth", action, validActions);
|
|
6779
|
+
const result = await authMeta.handler(flags);
|
|
6780
|
+
writeCommandResult(result);
|
|
5922
6781
|
return;
|
|
5923
6782
|
}
|
|
5924
|
-
// install-skill — dispatched before client creation
|
|
5925
6783
|
if (resource === "install-skill") {
|
|
5926
|
-
const
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
}
|
|
5931
|
-
else {
|
|
5932
|
-
const lines = [
|
|
5933
|
-
`Usage: ${INSTALL_SKILL_USAGE}`,
|
|
5934
|
-
"",
|
|
5935
|
-
INSTALL_SKILL_DESCRIPTION,
|
|
5936
|
-
"",
|
|
5937
|
-
"Flags:",
|
|
5938
|
-
" --global Install to user-level global scope (default: project)",
|
|
5939
|
-
" --agent NAME Target a specific agent: claude, codex, cursor, pi, omp",
|
|
5940
|
-
" --target PATH Project directory to install into (default: workspace root)",
|
|
5941
|
-
" --list-agents Print detected agents and exit",
|
|
5942
|
-
" --dry-run Print planned skill installs without writing files",
|
|
5943
|
-
" --plan Print planned skill installs without writing files",
|
|
5944
|
-
];
|
|
5945
|
-
process.stderr.write(`${lines.join("\n")}\n`);
|
|
5946
|
-
}
|
|
5947
|
-
process.exit(0);
|
|
5948
|
-
}
|
|
5949
|
-
if (installSkillAction !== undefined && installSkillAction !== "run") {
|
|
5950
|
-
throw new UsageError(`Usage: ${INSTALL_SKILL_USAGE}`);
|
|
6784
|
+
const action = positional[1];
|
|
6785
|
+
currentCommandContext.action = action ?? "run";
|
|
6786
|
+
if (action !== undefined && action !== "run") {
|
|
6787
|
+
throw unknownActionError("install-skill", action, ["run",]);
|
|
5951
6788
|
}
|
|
5952
|
-
const listOnly = flags["list-agents"] === true;
|
|
5953
6789
|
const agentFilter = typeof flags["agent"] === "string" ? flags["agent"] : undefined;
|
|
5954
6790
|
const isGlobal = flags["global"] === true;
|
|
5955
6791
|
const targetDir = typeof flags["target"] === "string" ? flags["target"] : undefined;
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
6792
|
+
const targets = (() => {
|
|
6793
|
+
if (!agentFilter)
|
|
6794
|
+
return detectAgents();
|
|
5959
6795
|
const def = AGENTS[agentFilter];
|
|
5960
6796
|
if (!def) {
|
|
5961
|
-
throw new UsageError(`Unknown agent: ${agentFilter}
|
|
6797
|
+
throw new UsageError(`Unknown agent: ${agentFilter}.`, "usage_error", COMMANDS_RUN_HINT, { agent: agentFilter, validAgents: Object.keys(AGENTS), });
|
|
5962
6798
|
}
|
|
5963
|
-
|
|
5964
|
-
}
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
for (const t of targets) {
|
|
5975
|
-
process.stderr.write(` ${t.id} (${t.def.name}, via ${t.via})\n`);
|
|
5976
|
-
}
|
|
5977
|
-
}
|
|
5978
|
-
process.exit(0);
|
|
6799
|
+
return [{ id: agentFilter, def, via: "flag", },];
|
|
6800
|
+
})();
|
|
6801
|
+
if (flags["list-agents"] === true) {
|
|
6802
|
+
writeCommandResult({
|
|
6803
|
+
agents: targets.map((target) => ({
|
|
6804
|
+
id: target.id,
|
|
6805
|
+
name: target.def.name,
|
|
6806
|
+
via: target.via,
|
|
6807
|
+
})),
|
|
6808
|
+
});
|
|
6809
|
+
return;
|
|
5979
6810
|
}
|
|
5980
6811
|
if (targets.length === 0) {
|
|
5981
|
-
throw new UsageError("No coding agents detected.
|
|
6812
|
+
throw new UsageError("No coding agents detected.", "usage_error", "Use --agent NAME to choose one of the supported agents.", { validAgents: Object.keys(AGENTS), });
|
|
5982
6813
|
}
|
|
5983
6814
|
const scope = isGlobal ? "global" : "project";
|
|
5984
6815
|
const cwd = targetDir ?? (isGlobal ? process.cwd() : findWorkspaceRoot(process.cwd()));
|
|
6816
|
+
const installed = planSkillInstalls(targets, { global: isGlobal, cwd, });
|
|
5985
6817
|
if (flags["plan"] === true) {
|
|
5986
6818
|
writeCommandResult(planResult("install-skill", "run", {
|
|
5987
6819
|
identifiers: { scope, target: cwd, },
|
|
5988
|
-
payload: {
|
|
5989
|
-
agents: targets.map((target) => ({
|
|
5990
|
-
id: target.id,
|
|
5991
|
-
name: target.def.name,
|
|
5992
|
-
via: target.via,
|
|
5993
|
-
})),
|
|
5994
|
-
},
|
|
6820
|
+
payload: { installed, },
|
|
5995
6821
|
idempotency: "none",
|
|
5996
6822
|
asyncKind: "none",
|
|
5997
6823
|
exitCodesOnFailure: { usage: 1, error: 2, transient: 3, },
|
|
@@ -5999,163 +6825,93 @@ async function main() {
|
|
|
5999
6825
|
}));
|
|
6000
6826
|
return;
|
|
6001
6827
|
}
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
id: target.id,
|
|
6011
|
-
name: target.def.name,
|
|
6012
|
-
via: target.via,
|
|
6013
|
-
})),
|
|
6014
|
-
});
|
|
6015
|
-
return;
|
|
6016
|
-
}
|
|
6017
|
-
process.stderr.write(`Installing dataiku-dss skill (${scope} scope):\n`);
|
|
6018
|
-
const results = installSkill(targets, { global: isGlobal, cwd, });
|
|
6019
|
-
for (const r of results) {
|
|
6020
|
-
process.stderr.write(` ${r.agent} -> ${r.path}\n`);
|
|
6021
|
-
}
|
|
6022
|
-
if (results.length > 0) {
|
|
6023
|
-
process.stderr.write(`\nDone. ${results.length} skill(s) installed.\n`);
|
|
6024
|
-
}
|
|
6828
|
+
writeCommandResult({
|
|
6829
|
+
scope,
|
|
6830
|
+
target: cwd,
|
|
6831
|
+
installed: flags["dry-run"] === true
|
|
6832
|
+
? installed
|
|
6833
|
+
: installSkill(targets, { global: isGlobal, cwd, }),
|
|
6834
|
+
...(flags["dry-run"] === true ? { dryRun: true, } : {}),
|
|
6835
|
+
});
|
|
6025
6836
|
return;
|
|
6026
6837
|
}
|
|
6027
|
-
// commands — machine-readable introspection (no auth needed)
|
|
6028
6838
|
if (resource === "commands") {
|
|
6029
6839
|
const action = positional[1];
|
|
6030
|
-
if (
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
...COMMANDS_EXAMPLES.map((example) => ` ${example}`),
|
|
6042
|
-
];
|
|
6043
|
-
process.stderr.write(`${lines.join("\n")}\n`);
|
|
6044
|
-
}
|
|
6045
|
-
process.exit(0);
|
|
6046
|
-
}
|
|
6840
|
+
if (!action)
|
|
6841
|
+
throw missingActionError("commands", ["run",], COMMANDS_USAGE);
|
|
6842
|
+
currentCommandContext.action = action;
|
|
6843
|
+
if (action !== "run")
|
|
6844
|
+
throw unknownActionError("commands", action, ["run",]);
|
|
6845
|
+
writeCommandResult(buildCommandRegistry());
|
|
6846
|
+
return;
|
|
6847
|
+
}
|
|
6848
|
+
if (resource === "version") {
|
|
6849
|
+
const action = positional[1];
|
|
6850
|
+
currentCommandContext.action = action ?? "run";
|
|
6047
6851
|
if (action !== undefined && action !== "run") {
|
|
6048
|
-
throw
|
|
6852
|
+
throw unknownActionError("version", action, ["run",]);
|
|
6049
6853
|
}
|
|
6050
|
-
writeCommandResult(
|
|
6854
|
+
writeCommandResult(cliVersionResult());
|
|
6051
6855
|
return;
|
|
6052
6856
|
}
|
|
6053
6857
|
if (resource === "cleanup") {
|
|
6054
6858
|
const action = positional[1];
|
|
6055
|
-
|
|
6056
|
-
if (flags["report-json"] === true) {
|
|
6057
|
-
writeReportHelp("cleanup", "run");
|
|
6058
|
-
}
|
|
6059
|
-
else {
|
|
6060
|
-
const lines = [
|
|
6061
|
-
`Usage: ${CLEANUP_USAGE}`,
|
|
6062
|
-
"",
|
|
6063
|
-
CLEANUP_DESCRIPTION,
|
|
6064
|
-
"",
|
|
6065
|
-
"Examples:",
|
|
6066
|
-
...CLEANUP_EXAMPLES.map((example) => ` ${example}`),
|
|
6067
|
-
];
|
|
6068
|
-
process.stderr.write(`${lines.join("\n")}\n`);
|
|
6069
|
-
}
|
|
6070
|
-
process.exit(0);
|
|
6071
|
-
}
|
|
6859
|
+
currentCommandContext.action = action ?? "run";
|
|
6072
6860
|
if (action !== undefined && action !== "run") {
|
|
6073
|
-
throw
|
|
6861
|
+
throw unknownActionError("cleanup", action, ["run",]);
|
|
6074
6862
|
}
|
|
6075
6863
|
const { result, exitCode, } = await runCleanup(flags);
|
|
6076
6864
|
writeCommandResult(result);
|
|
6077
|
-
|
|
6865
|
+
if (exitCode !== 0)
|
|
6866
|
+
process.exit(exitCode);
|
|
6867
|
+
return;
|
|
6078
6868
|
}
|
|
6079
6869
|
if (resource === "fixtures") {
|
|
6080
6870
|
const action = positional[1];
|
|
6081
|
-
currentCommandContext.action = "run";
|
|
6082
|
-
if (flags["help"] === true) {
|
|
6083
|
-
if (flags["report-json"] === true) {
|
|
6084
|
-
writeReportHelp("fixtures", "run");
|
|
6085
|
-
}
|
|
6086
|
-
else {
|
|
6087
|
-
const lines = [
|
|
6088
|
-
`Usage: ${FIXTURES_USAGE}`,
|
|
6089
|
-
"",
|
|
6090
|
-
FIXTURES_DESCRIPTION,
|
|
6091
|
-
"",
|
|
6092
|
-
"Examples:",
|
|
6093
|
-
...FIXTURES_EXAMPLES.map((example) => ` ${example}`),
|
|
6094
|
-
];
|
|
6095
|
-
process.stderr.write(`${lines.join("\n")}\n`);
|
|
6096
|
-
}
|
|
6097
|
-
process.exit(0);
|
|
6098
|
-
}
|
|
6871
|
+
currentCommandContext.action = action ?? "run";
|
|
6099
6872
|
if (action !== undefined && action !== "run") {
|
|
6100
|
-
throw
|
|
6873
|
+
throw unknownActionError("fixtures", action, ["run",]);
|
|
6101
6874
|
}
|
|
6102
6875
|
const result = await runFixtures(flags);
|
|
6103
6876
|
writeCommandResult(result);
|
|
6104
6877
|
return;
|
|
6105
6878
|
}
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
}
|
|
6112
|
-
if (flags["report-json"] === true) {
|
|
6113
|
-
throw new UsageError(`Unknown resource: ${resource}. Available: ${RESOURCE_NAMES.join(", ")}`);
|
|
6114
|
-
}
|
|
6115
|
-
process.stderr.write(`Unknown resource: ${resource} \nAvailable: ${RESOURCE_NAMES.join(", ")} \n`);
|
|
6116
|
-
process.exit(1);
|
|
6117
|
-
}
|
|
6118
|
-
// Resource-level help
|
|
6119
|
-
if (positional.length === 1 || flags["help"] === true) {
|
|
6120
|
-
if (positional.length === 1) {
|
|
6121
|
-
printResourceHelp(resource);
|
|
6122
|
-
if (flags["help"])
|
|
6123
|
-
process.exit(0);
|
|
6124
|
-
process.exit(1);
|
|
6125
|
-
}
|
|
6126
|
-
}
|
|
6127
|
-
const action = positional[1];
|
|
6128
|
-
const actionMeta = commands[resource][action];
|
|
6129
|
-
// Unknown action
|
|
6130
|
-
if (!actionMeta) {
|
|
6131
|
-
if (flags["report-json"] === true) {
|
|
6132
|
-
throw new UsageError(`Unknown action: ${resource} ${action}. Available actions for ${resource}: ${Object.keys(commands[resource]).join(", ")}`);
|
|
6879
|
+
if (resource === "batch") {
|
|
6880
|
+
const action = positional[1];
|
|
6881
|
+
currentCommandContext.action = action ?? "run";
|
|
6882
|
+
if (action !== undefined && action !== "run") {
|
|
6883
|
+
throw unknownActionError("batch", action, ["run",]);
|
|
6133
6884
|
}
|
|
6134
|
-
|
|
6135
|
-
|
|
6885
|
+
const { result, exitCode, } = await runBatch(flags);
|
|
6886
|
+
writeCommandResult(result);
|
|
6887
|
+
if (exitCode !== 0)
|
|
6888
|
+
process.exit(exitCode);
|
|
6889
|
+
return;
|
|
6136
6890
|
}
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
printActionHelp(resource, action);
|
|
6143
|
-
process.exit(0);
|
|
6891
|
+
if (!commands[resource])
|
|
6892
|
+
throw unknownResourceError(resource);
|
|
6893
|
+
const resourceActions = commands[resource];
|
|
6894
|
+
if (positional.length === 1) {
|
|
6895
|
+
throw missingActionError(resource, Object.keys(resourceActions), `dss ${resource} <action> [args...]`);
|
|
6144
6896
|
}
|
|
6897
|
+
const action = positional[1];
|
|
6898
|
+
currentCommandContext.action = action;
|
|
6899
|
+
const actionMeta = resourceActions[action];
|
|
6900
|
+
if (!actionMeta)
|
|
6901
|
+
throw unknownActionError(resource, action, Object.keys(resourceActions));
|
|
6145
6902
|
const args = positional.slice(2);
|
|
6146
6903
|
if (flags["plan"] === true) {
|
|
6147
6904
|
const plan = buildMutationPlan(resource, action, actionMeta, args, flags);
|
|
6148
6905
|
writeCommandResult(plan);
|
|
6149
6906
|
return;
|
|
6150
6907
|
}
|
|
6151
|
-
// Resolve credentials: flags > env > saved > .env
|
|
6152
6908
|
const { url, apiKey, projectKey, tlsRejectUnauthorized, caCertPath, } = resolveCredentials(flags);
|
|
6153
6909
|
currentCommandContext.projectKey = projectKey;
|
|
6154
6910
|
if (!url) {
|
|
6155
|
-
throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL
|
|
6911
|
+
throw new UsageError("Missing Dataiku URL.", "missing_required_flag", "Set DATAIKU_URL or pass --url.", { requiredFlags: ["url",], env: ["DATAIKU_URL",], });
|
|
6156
6912
|
}
|
|
6157
6913
|
if (!apiKey) {
|
|
6158
|
-
throw new UsageError("Missing API key. Set DATAIKU_API_KEY
|
|
6914
|
+
throw new UsageError("Missing API key.", "missing_required_flag", "Set DATAIKU_API_KEY or pass --api-key.", { requiredFlags: ["api-key",], env: ["DATAIKU_API_KEY",], });
|
|
6159
6915
|
}
|
|
6160
6916
|
const requestTimeoutMs = num(flags["request-timeout"]);
|
|
6161
6917
|
const retryMaxAttempts = num(flags["retries"]);
|
|
@@ -6180,41 +6936,17 @@ async function main() {
|
|
|
6180
6936
|
if (entry)
|
|
6181
6937
|
await appendCleanupLedgerEntry(flags["record-cleanup"], entry);
|
|
6182
6938
|
}
|
|
6183
|
-
|
|
6939
|
+
const failureExitCode = commandFailureExitCode(result);
|
|
6940
|
+
if (failureExitCode !== undefined)
|
|
6941
|
+
throw new CommandResultFailure(result, failureExitCode);
|
|
6942
|
+
if (flags["raw"] === true && typeof result === "string" && typeof flags["output"] !== "string") {
|
|
6184
6943
|
process.stdout.write(result);
|
|
6185
6944
|
}
|
|
6186
6945
|
else {
|
|
6187
6946
|
writeCommandResult(result);
|
|
6188
6947
|
}
|
|
6189
|
-
const failureExitCode = commandFailureExitCode(result);
|
|
6190
|
-
if (failureExitCode !== undefined)
|
|
6191
|
-
process.exit(failureExitCode);
|
|
6192
6948
|
}
|
|
6193
6949
|
main().catch((err) => {
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
if (err instanceof UsageError)
|
|
6197
|
-
process.exit(1);
|
|
6198
|
-
if (err instanceof DataikuError)
|
|
6199
|
-
process.exit(err.category === "transient" ? 3 : 2);
|
|
6200
|
-
process.exit(2);
|
|
6201
|
-
}
|
|
6202
|
-
if (err instanceof UsageError) {
|
|
6203
|
-
process.stderr.write(`${JSON.stringify({ error: err.message, code: "usage", }, null, 2)}\n`);
|
|
6204
|
-
process.exit(1);
|
|
6205
|
-
}
|
|
6206
|
-
if (err instanceof DataikuError) {
|
|
6207
|
-
const payload = {
|
|
6208
|
-
error: err.message,
|
|
6209
|
-
category: err.category,
|
|
6210
|
-
retryable: err.retryable,
|
|
6211
|
-
};
|
|
6212
|
-
if (err.retryHint)
|
|
6213
|
-
payload.retryHint = err.retryHint;
|
|
6214
|
-
process.stderr.write(`${JSON.stringify(payload, null, 2)} \n`);
|
|
6215
|
-
process.exit(err.category === "transient" ? 3 : 2);
|
|
6216
|
-
}
|
|
6217
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
6218
|
-
process.stderr.write(`${JSON.stringify({ error: message, }, null, 2)} \n`);
|
|
6219
|
-
process.exit(1);
|
|
6950
|
+
writeErrorReport(err);
|
|
6951
|
+
process.exit(errorExitCode(err));
|
|
6220
6952
|
});
|