dataiku-sdk 1.0.0 → 2.0.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 +64 -10
- package/bin/dss.js +67 -78
- package/dist/build-metadata.json +1 -0
- package/dist/src/cli/commands/api-service.js +5 -4
- package/dist/src/cli/commands/app.js +45 -12
- package/dist/src/cli/commands/bundle.js +5 -4
- package/dist/src/cli/commands/data-quality.js +42 -0
- package/dist/src/cli/commands/dataset.d.ts +23 -0
- package/dist/src/cli/commands/dataset.js +187 -6
- package/dist/src/cli/commands/job.js +15 -2
- package/dist/src/cli/commands/project-library.js +4 -4
- package/dist/src/cli/commands/project.js +116 -12
- package/dist/src/cli/contract.d.ts +1 -0
- package/dist/src/cli/contract.js +194 -15
- package/dist/src/cli/doctor.d.ts +12 -2
- package/dist/src/cli/doctor.js +44 -13
- package/dist/src/cli/flags.js +2 -0
- package/dist/src/cli/helpers/app-successor.d.ts +15 -3
- package/dist/src/cli/helpers/app-successor.js +155 -15
- package/dist/src/cli/helpers/cleanup.js +30 -1
- package/dist/src/cli/output.d.ts +3 -1
- package/dist/src/cli/output.js +11 -6
- package/dist/src/cli/types.d.ts +2 -0
- package/dist/src/cli/version.d.ts +47 -4
- package/dist/src/cli/version.js +176 -9
- package/dist/src/cli.js +55 -14
- package/dist/src/client.d.ts +28 -1
- package/dist/src/client.js +34 -7
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.js +1 -0
- package/dist/src/resources/datasets.d.ts +43 -0
- package/dist/src/resources/datasets.js +199 -28
- package/dist/src/resources/folders.js +6 -8
- package/dist/src/resources/projects.d.ts +36 -3
- package/dist/src/resources/projects.js +122 -20
- package/dist/src/skill.js +1 -18
- package/dist/src/utils/project-archive.d.ts +32 -0
- package/dist/src/utils/project-archive.js +623 -0
- package/dist/src/utils/stable-hash.js +1 -2
- package/package.json +10 -10
- package/plugin.json +1 -1
- package/skills/dataiku-dss/SKILL.md +27 -35
|
@@ -150,6 +150,7 @@ export async function verifyAppInstance(client, appId, projectKey, expectVersion
|
|
|
150
150
|
}
|
|
151
151
|
const SUCCESSOR_REMEDIATION = "The successor project may exist: replay the recorded cleanup entry, which future-gates deletion when DSS supplied a future ID. Without a cleanup entry, inspect the creation future and wait for it to become terminal before deleting the requested successor project. The predecessor project was not modified.";
|
|
152
152
|
const CREATE_REJECTED_REMEDIATION = "DSS refused the creation request, so no successor project was created: fix the reported problem and retry. The predecessor project was not modified.";
|
|
153
|
+
const SUCCESSOR_UNBOUND_REMEDIATION = "DSS may have accepted the successor request, but no future ID or project incarnation is available to authorize cleanup. Inspect the app's instances and DSS task history; do not delete the requested key until creation is known terminal and its identity is verified. The predecessor project was not modified.";
|
|
153
154
|
/**
|
|
154
155
|
* True only when DSS definitively refused a write: a non-retryable 4xx with a
|
|
155
156
|
* classified client-side taxonomy. Transport failures (status 0), 5xx, retryable
|
|
@@ -186,7 +187,19 @@ export function futureTargetIdentity(result) {
|
|
|
186
187
|
* A hidden 403 is not absence: project listing can prove a collision, but it
|
|
187
188
|
* cannot prove that an inaccessible project does not exist.
|
|
188
189
|
*/
|
|
189
|
-
export async function requireAbsentProjectTarget(client, targetProjectKey, targetFlag) {
|
|
190
|
+
export async function requireAbsentProjectTarget(client, targetProjectKey, targetFlag, context) {
|
|
191
|
+
if (context.visibleAppInstanceKeys?.includes(targetProjectKey) === true) {
|
|
192
|
+
throw new UsageError(`Target project ${targetProjectKey} already exists as an app instance.`, "validation_failed", `Choose an unused ${targetFlag}: app instance creation never writes into an existing project.`, {
|
|
193
|
+
targetProjectKey,
|
|
194
|
+
targetFlag,
|
|
195
|
+
directTargetProbe: "not-executed",
|
|
196
|
+
targetVisibleInProjectList: null,
|
|
197
|
+
targetVisibleInAppInstances: true,
|
|
198
|
+
appInstancesProbe: 200,
|
|
199
|
+
preflightExecuted: true,
|
|
200
|
+
creationPostAttempted: false,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
190
203
|
try {
|
|
191
204
|
const existing = await readIfExists(() => client.projects.get(targetProjectKey));
|
|
192
205
|
if (existing === undefined)
|
|
@@ -195,10 +208,61 @@ export async function requireAbsentProjectTarget(client, targetProjectKey, targe
|
|
|
195
208
|
catch (error) {
|
|
196
209
|
if (!(error instanceof DataikuError) || error.status !== 403)
|
|
197
210
|
throw error;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
211
|
+
let projectListProbe = 200;
|
|
212
|
+
let targetVisibleInProjectList;
|
|
213
|
+
try {
|
|
214
|
+
const accessible = await client.projects.list();
|
|
215
|
+
targetVisibleInProjectList = accessible.some((project) => project.projectKey === targetProjectKey);
|
|
216
|
+
}
|
|
217
|
+
catch (projectListError) {
|
|
218
|
+
if (!(projectListError instanceof DataikuError) || projectListError.status !== 403) {
|
|
219
|
+
throw projectListError;
|
|
220
|
+
}
|
|
221
|
+
projectListProbe = 403;
|
|
222
|
+
targetVisibleInProjectList = null;
|
|
201
223
|
}
|
|
224
|
+
let appInstancesProbe = 200;
|
|
225
|
+
let visibleAppInstanceKeys = context.visibleAppInstanceKeys;
|
|
226
|
+
if (visibleAppInstanceKeys === undefined) {
|
|
227
|
+
try {
|
|
228
|
+
visibleAppInstanceKeys = (await client.applications.listInstances(context.appId))
|
|
229
|
+
.map((instance) => instance.projectKey);
|
|
230
|
+
}
|
|
231
|
+
catch (appInstancesError) {
|
|
232
|
+
if (!(appInstancesError instanceof DataikuError) || appInstancesError.status !== 403) {
|
|
233
|
+
throw appInstancesError;
|
|
234
|
+
}
|
|
235
|
+
appInstancesProbe = 403;
|
|
236
|
+
visibleAppInstanceKeys = [];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const targetVisibleInAppInstances = appInstancesProbe === 200
|
|
240
|
+
? visibleAppInstanceKeys?.includes(targetProjectKey) ?? false
|
|
241
|
+
: null;
|
|
242
|
+
const probeDetails = {
|
|
243
|
+
targetProjectKey,
|
|
244
|
+
targetFlag,
|
|
245
|
+
directTargetProbe: 403,
|
|
246
|
+
projectListProbe,
|
|
247
|
+
targetVisibleInProjectList,
|
|
248
|
+
targetVisibleInAppInstances,
|
|
249
|
+
appInstancesProbe,
|
|
250
|
+
preflightExecuted: true,
|
|
251
|
+
creationPostAttempted: false,
|
|
252
|
+
};
|
|
253
|
+
if (targetVisibleInProjectList !== true && targetVisibleInAppInstances !== true) {
|
|
254
|
+
throw new UsageError(`Could not confirm target project ${targetProjectKey} is absent.`, "target_absence_unverifiable", "DSS exposes no permission-independent project-key availability endpoint, and app-instance creation has no published atomic duplicate-key guarantee. Retry with an identity that has global project visibility; creation was not attempted.", {
|
|
255
|
+
...probeDetails,
|
|
256
|
+
targetProbe: "forbidden-and-not-listable",
|
|
257
|
+
deploymentMasksUnknownProjects: "possible-but-unproven",
|
|
258
|
+
supportedRecoveryModes: ["grant-global-project-visibility",],
|
|
259
|
+
unavailableRecoveryModes: [
|
|
260
|
+
"use-supported-key-availability-endpoint",
|
|
261
|
+
"server-atomic-create",
|
|
262
|
+
],
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
throw new UsageError(`Target project ${targetProjectKey} already exists.`, "validation_failed", `Choose an unused ${targetFlag}: app instance creation never writes into an existing project.`, probeDetails);
|
|
202
266
|
}
|
|
203
267
|
throw new UsageError(`Target project ${targetProjectKey} already exists.`, "validation_failed", `Choose an unused ${targetFlag}: app instance creation never writes into an existing project.`, { targetProjectKey, targetFlag, });
|
|
204
268
|
}
|
|
@@ -226,12 +290,26 @@ async function preflightSuccessor(client, request, usage) {
|
|
|
226
290
|
if (source.projectAppType !== "APP_INSTANCE") {
|
|
227
291
|
throw new UsageError(`Project ${from} is a ${source.projectAppType ?? "non-app"} project, not a classic Dataiku App instance.`, "validation_failed", "--from must name an APP_INSTANCE project; app templates are versioned with `dss app set-manifest-version`.", { sourceProjectKey: from, projectAppType: source.projectAppType ?? null, });
|
|
228
292
|
}
|
|
229
|
-
await requireAbsentProjectTarget(client, to, "--to"
|
|
293
|
+
await requireAbsentProjectTarget(client, to, "--to", {
|
|
294
|
+
appId,
|
|
295
|
+
visibleAppInstanceKeys: instances.map((instance) => instance.projectKey),
|
|
296
|
+
});
|
|
230
297
|
const templateManifest = await client.applications.getAppManifest(appId);
|
|
231
298
|
const templateVersion = rawVersion(templateManifest);
|
|
232
299
|
if (templateVersion === null) {
|
|
233
300
|
throw new UsageError(`App template ${appId} has no manifest version, so a successor instance cannot be verified against it.`, "validation_failed", "Set the template version first with `dss app set-manifest-version --manifest-version V --project-key <templateProjectKey>`.", { appId, });
|
|
234
301
|
}
|
|
302
|
+
const templateReferenceValidation = await client.applications.validateAppManifest(templateManifest, appId);
|
|
303
|
+
if (!templateReferenceValidation.valid) {
|
|
304
|
+
throw new UsageError(`App template ${appId} failed manifest reference validation.`, "validation_failed", "Fix the template manifest references before changing its version or creating a successor.", {
|
|
305
|
+
appId,
|
|
306
|
+
manifestHash: templateReferenceValidation.manifestHash,
|
|
307
|
+
checks: templateReferenceValidation.checks,
|
|
308
|
+
errors: templateReferenceValidation.errors,
|
|
309
|
+
preflightExecuted: true,
|
|
310
|
+
creationPostAttempted: false,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
235
313
|
const sourcePermissions = request.copyPermissions
|
|
236
314
|
? await client.projects.getPermissions(from)
|
|
237
315
|
: undefined;
|
|
@@ -243,6 +321,7 @@ async function preflightSuccessor(client, request, usage) {
|
|
|
243
321
|
targetExists: false,
|
|
244
322
|
templateManifestHash: stableHash(templateManifest),
|
|
245
323
|
templateVersion,
|
|
324
|
+
templateReferenceValidation,
|
|
246
325
|
...(sourcePermissions
|
|
247
326
|
? {
|
|
248
327
|
sourcePermissions,
|
|
@@ -251,6 +330,52 @@ async function preflightSuccessor(client, request, usage) {
|
|
|
251
330
|
: {}),
|
|
252
331
|
};
|
|
253
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Run every read-only gate used by successor creation without changing the
|
|
335
|
+
* template version or issuing the creation POST. Its manifest hash is suitable
|
|
336
|
+
* for the subsequent set-manifest-version --expect-hash guard.
|
|
337
|
+
*/
|
|
338
|
+
export async function preflightSuccessorInstance(client, request, usage) {
|
|
339
|
+
const preflight = await preflightSuccessor(client, request, usage);
|
|
340
|
+
return {
|
|
341
|
+
action: "successor-preflight",
|
|
342
|
+
preflight: "passed",
|
|
343
|
+
preflightExecuted: true,
|
|
344
|
+
creationPostAttempted: false,
|
|
345
|
+
versionMutationAttempted: false,
|
|
346
|
+
appId: request.appId,
|
|
347
|
+
source: {
|
|
348
|
+
projectKey: request.from,
|
|
349
|
+
projectAppType: preflight.sourceProjectAppType,
|
|
350
|
+
version: preflight.sourceVersion,
|
|
351
|
+
manifestHash: preflight.sourceManifestHash,
|
|
352
|
+
...(preflight.sourcePermissionsHash
|
|
353
|
+
? { permissionsHash: preflight.sourcePermissionsHash, }
|
|
354
|
+
: {}),
|
|
355
|
+
},
|
|
356
|
+
target: {
|
|
357
|
+
projectKey: request.to,
|
|
358
|
+
name: preflight.targetProjectName,
|
|
359
|
+
exists: false,
|
|
360
|
+
},
|
|
361
|
+
targetPreflight: "confirmed-absent",
|
|
362
|
+
template: {
|
|
363
|
+
projectKey: request.appId,
|
|
364
|
+
version: preflight.templateVersion,
|
|
365
|
+
manifestHash: preflight.templateManifestHash,
|
|
366
|
+
referenceValidation: {
|
|
367
|
+
valid: preflight.templateReferenceValidation.valid,
|
|
368
|
+
checks: preflight.templateReferenceValidation.checks,
|
|
369
|
+
errors: preflight.templateReferenceValidation.errors,
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
copyPermissions: request.copyPermissions,
|
|
373
|
+
next: {
|
|
374
|
+
setVersion: `dss app set-manifest-version --manifest-version <VERSION> --expect-hash ${preflight.templateManifestHash} --project-key ${request.appId}`,
|
|
375
|
+
createSuccessor: `dss app create-successor-instance ${request.appId} --from ${request.from} --to ${request.to}${request.name !== undefined ? ` --name ${JSON.stringify(request.name)}` : ""}${request.copyPermissions ? " --copy-permissions" : ""}`,
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
}
|
|
254
379
|
/**
|
|
255
380
|
* Create a new app instance from the current template version alongside an
|
|
256
381
|
* existing instance. Classic app instances have no public upgrade endpoint, so
|
|
@@ -258,8 +383,9 @@ async function preflightSuccessor(client, request, usage) {
|
|
|
258
383
|
* and deliberately retired.
|
|
259
384
|
*
|
|
260
385
|
* After the creation POST this function never throws. A post-POST failure
|
|
261
|
-
* returns a failed-wait result
|
|
262
|
-
*
|
|
386
|
+
* returns a failed-wait result. Only a DSS future ID or verified project
|
|
387
|
+
* incarnation can authorize cleanup; an ambiguous POST with neither stays
|
|
388
|
+
* indeterminate and produces no cleanup entry.
|
|
263
389
|
*/
|
|
264
390
|
export async function createSuccessorInstance(client, request, usage) {
|
|
265
391
|
const { appId, from, to, } = request;
|
|
@@ -312,12 +438,17 @@ export async function createSuccessorInstance(client, request, usage) {
|
|
|
312
438
|
projectKey: to,
|
|
313
439
|
target: { projectKey: to, name: preflight.targetProjectName, },
|
|
314
440
|
success: false,
|
|
315
|
-
state: "CREATE_FAILED",
|
|
441
|
+
state: rejected ? "CREATE_FAILED" : "INDETERMINATE",
|
|
316
442
|
elapsedMs: 0,
|
|
317
443
|
pollCount: 0,
|
|
318
444
|
stage: "create",
|
|
319
|
-
|
|
320
|
-
|
|
445
|
+
outcome: rejected ? "rejected" : "indeterminate",
|
|
446
|
+
creationPostAttempted: true,
|
|
447
|
+
cleanupEligible: false,
|
|
448
|
+
recoveryCommands: rejected
|
|
449
|
+
? []
|
|
450
|
+
: [`dss app instances ${appId}`,],
|
|
451
|
+
remediation: rejected ? CREATE_REJECTED_REMEDIATION : SUCCESSOR_UNBOUND_REMEDIATION,
|
|
321
452
|
...safeErrorSummary(error),
|
|
322
453
|
};
|
|
323
454
|
}
|
|
@@ -342,11 +473,13 @@ export async function createSuccessorInstance(client, request, usage) {
|
|
|
342
473
|
stage: "create",
|
|
343
474
|
...(instance ? { instance, } : {}),
|
|
344
475
|
...(jobId !== undefined ? { futureId: jobId, jobId, } : {}),
|
|
345
|
-
cleanupEligible:
|
|
476
|
+
cleanupEligible: jobId !== undefined,
|
|
346
477
|
error: "DSS named a different target project in the creation response.",
|
|
347
478
|
expected: { projectKey: to, },
|
|
348
479
|
actual: { projectKey: echoedTarget, },
|
|
349
|
-
remediation:
|
|
480
|
+
remediation: jobId !== undefined
|
|
481
|
+
? SUCCESSOR_REMEDIATION
|
|
482
|
+
: SUCCESSOR_UNBOUND_REMEDIATION,
|
|
350
483
|
};
|
|
351
484
|
}
|
|
352
485
|
const base = {
|
|
@@ -381,13 +514,16 @@ export async function createSuccessorInstance(client, request, usage) {
|
|
|
381
514
|
: "scalar",
|
|
382
515
|
}),
|
|
383
516
|
success: false,
|
|
384
|
-
state: "
|
|
517
|
+
state: "INDETERMINATE",
|
|
385
518
|
elapsedMs: 0,
|
|
386
519
|
pollCount: 0,
|
|
387
520
|
stage: "future-wait",
|
|
388
|
-
|
|
521
|
+
outcome: "indeterminate",
|
|
522
|
+
creationPostAttempted: true,
|
|
523
|
+
cleanupEligible: false,
|
|
389
524
|
error: "DSS returned no instance-creation future ID, so the successor instance cannot be verified.",
|
|
390
|
-
remediation:
|
|
525
|
+
remediation: SUCCESSOR_UNBOUND_REMEDIATION,
|
|
526
|
+
recoveryCommands: [`dss app instances ${appId}`,],
|
|
391
527
|
};
|
|
392
528
|
}
|
|
393
529
|
else {
|
|
@@ -408,6 +544,7 @@ export async function createSuccessorInstance(client, request, usage) {
|
|
|
408
544
|
pollCount: 0,
|
|
409
545
|
stage: "future-wait",
|
|
410
546
|
cleanupEligible: true,
|
|
547
|
+
recoveryCommands: [`dss future wait ${jobId}`, `dss app instances ${appId}`,],
|
|
411
548
|
remediation: SUCCESSOR_REMEDIATION,
|
|
412
549
|
...safeErrorSummary(error),
|
|
413
550
|
};
|
|
@@ -451,6 +588,9 @@ export async function createSuccessorInstance(client, request, usage) {
|
|
|
451
588
|
if (!waited.success) {
|
|
452
589
|
return failure(waited.state, "future-wait", {
|
|
453
590
|
error: "The instance-creation future did not complete successfully.",
|
|
591
|
+
recoveryCommands: jobId !== undefined
|
|
592
|
+
? [`dss future wait ${jobId}`, `dss app instances ${appId}`,]
|
|
593
|
+
: [`dss app instances ${appId}`,],
|
|
454
594
|
});
|
|
455
595
|
}
|
|
456
596
|
try {
|
|
@@ -4,7 +4,7 @@ export function projectArg(projectKey) {
|
|
|
4
4
|
}
|
|
5
5
|
export function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
|
|
6
6
|
if (!(action.startsWith("create") || action === "clone" || action === "duplicate"
|
|
7
|
-
|| action === "upload"))
|
|
7
|
+
|| action === "import" || action === "upload"))
|
|
8
8
|
return undefined;
|
|
9
9
|
const record = resultRecord(result);
|
|
10
10
|
if (record.skipped !== undefined)
|
|
@@ -190,6 +190,35 @@ export function cleanupLedgerEntry(resource, action, args, flags, result, projec
|
|
|
190
190
|
path: args[1],
|
|
191
191
|
cleanup: { argv: ["folder", "delete-file", args[0], args[1], ...withProject,], },
|
|
192
192
|
};
|
|
193
|
+
case "project.import": {
|
|
194
|
+
const usedProjectKey = stringField(record, ["usedProjectKey",]);
|
|
195
|
+
const recordedIncarnation = stringField(record, ["projectIncarnationHash",]);
|
|
196
|
+
const boundIncarnation = recordedIncarnation
|
|
197
|
+
&& /^[0-9a-f]{64}$/.test(recordedIncarnation)
|
|
198
|
+
? recordedIncarnation
|
|
199
|
+
: undefined;
|
|
200
|
+
// Only a verified success may produce a cleanup entry, and it must
|
|
201
|
+
// bind the concrete landed project incarnation: replaying an
|
|
202
|
+
// unbound delete could remove a replacement project that reused the
|
|
203
|
+
// key. Never emit legacy/unbound project entries.
|
|
204
|
+
if (!usedProjectKey || !boundIncarnation)
|
|
205
|
+
return undefined;
|
|
206
|
+
return {
|
|
207
|
+
...base,
|
|
208
|
+
projectKey: usedProjectKey,
|
|
209
|
+
name: usedProjectKey,
|
|
210
|
+
cleanup: {
|
|
211
|
+
argv: [
|
|
212
|
+
"project",
|
|
213
|
+
"delete",
|
|
214
|
+
usedProjectKey,
|
|
215
|
+
"--if-exists",
|
|
216
|
+
"--expect-project-incarnation",
|
|
217
|
+
boundIncarnation,
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
193
222
|
case "app.create-instance":
|
|
194
223
|
case "app.create-successor-instance": {
|
|
195
224
|
const targetKey = stringField(record, ["projectKey", "targetProjectKey",]);
|
package/dist/src/cli/output.d.ts
CHANGED
|
@@ -21,7 +21,8 @@ export declare function commandFailureExitCode(result: unknown): number | undefi
|
|
|
21
21
|
* Stable error code for a command-level failure report. A failed synchronous
|
|
22
22
|
* assertion (exit 4) is a distinct outcome from a failed long-running remote
|
|
23
23
|
* operation (also exit 4), so agents must never see `long_running_failure`
|
|
24
|
-
* for a result that finished synchronously with `unchanged: false
|
|
24
|
+
* for a result that finished synchronously with `unchanged: false` or
|
|
25
|
+
* `satisfied: false`.
|
|
25
26
|
*/
|
|
26
27
|
export declare function commandFailureCode(result: unknown, exitCode: number): StableErrorCode;
|
|
27
28
|
export declare class CommandResultFailure extends Error {
|
|
@@ -44,6 +45,7 @@ export declare function planResult(resource: string, action: string, options: {
|
|
|
44
45
|
payload?: unknown;
|
|
45
46
|
localWrites?: unknown;
|
|
46
47
|
plannedAndDryRun?: boolean;
|
|
48
|
+
requests?: unknown;
|
|
47
49
|
wait?: unknown;
|
|
48
50
|
}): Record<string, unknown>;
|
|
49
51
|
export declare function encodedProjectEndpoint(client: DataikuClient, projectKey: string | undefined, suffix: string): string;
|
package/dist/src/cli/output.js
CHANGED
|
@@ -87,10 +87,10 @@ export function isFailedWaitResult(result) {
|
|
|
87
87
|
&& (typeof record.state === "string" || typeof record.outcome === "string");
|
|
88
88
|
}
|
|
89
89
|
export function isAssertionFailureResult(result) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
if (result === null || typeof result !== "object" || Array.isArray(result))
|
|
91
|
+
return false;
|
|
92
|
+
const record = result;
|
|
93
|
+
return record.unchanged === false || record.satisfied === false;
|
|
94
94
|
}
|
|
95
95
|
function isNestedAssertionFailureResult(result) {
|
|
96
96
|
if (result === null || typeof result !== "object" || Array.isArray(result))
|
|
@@ -121,7 +121,8 @@ export function commandFailureExitCode(result) {
|
|
|
121
121
|
* Stable error code for a command-level failure report. A failed synchronous
|
|
122
122
|
* assertion (exit 4) is a distinct outcome from a failed long-running remote
|
|
123
123
|
* operation (also exit 4), so agents must never see `long_running_failure`
|
|
124
|
-
* for a result that finished synchronously with `unchanged: false
|
|
124
|
+
* for a result that finished synchronously with `unchanged: false` or
|
|
125
|
+
* `satisfied: false`.
|
|
125
126
|
*/
|
|
126
127
|
export function commandFailureCode(result, exitCode) {
|
|
127
128
|
if (isAssertionFailureResult(result) || isNestedAssertionFailureResult(result)) {
|
|
@@ -150,7 +151,10 @@ export function commandFailureMessage(result) {
|
|
|
150
151
|
return `Command completed with failed long-running result${state ? `: ${state}` : ""}.`;
|
|
151
152
|
}
|
|
152
153
|
if (isAssertionFailureResult(result)) {
|
|
153
|
-
|
|
154
|
+
const record = result;
|
|
155
|
+
return record.unchanged === false
|
|
156
|
+
? "Command completed with failed assertion result."
|
|
157
|
+
: "Command completed with unsatisfied assertion result.";
|
|
154
158
|
}
|
|
155
159
|
if (isNestedAssertionFailureResult(result)) {
|
|
156
160
|
return "Command completed with nested failed assertion result.";
|
|
@@ -188,6 +192,7 @@ export function planResult(resource, action, options) {
|
|
|
188
192
|
...(options.endpoint ? { endpoint: options.endpoint, } : {}),
|
|
189
193
|
...(options.payload !== undefined ? { payload: options.payload, } : {}),
|
|
190
194
|
...(options.localWrites !== undefined ? { localWrites: options.localWrites, } : {}),
|
|
195
|
+
...(options.requests !== undefined ? { requests: options.requests, } : {}),
|
|
191
196
|
...(options.wait !== undefined ? { wait: options.wait, } : {}),
|
|
192
197
|
idempotency: options.idempotency,
|
|
193
198
|
async: options.asyncKind,
|
package/dist/src/cli/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DataikuClient } from "../client.js";
|
|
2
2
|
export type CommandHandler = (client: DataikuClient, args: string[], flags: Record<string, string | boolean>) => Promise<unknown>;
|
|
3
|
+
export type LocalCommandHandler = (args: string[], flags: Record<string, string | boolean>) => Promise<unknown>;
|
|
3
4
|
export interface CommandPayloadSchema {
|
|
4
5
|
stdin?: boolean;
|
|
5
6
|
dataFlag?: boolean;
|
|
@@ -19,6 +20,7 @@ export interface CommandRegistryOverride {
|
|
|
19
20
|
}
|
|
20
21
|
export interface CommandMeta extends CommandRegistryOverride {
|
|
21
22
|
handler: CommandHandler;
|
|
23
|
+
localHandler?: LocalCommandHandler;
|
|
22
24
|
usage: string;
|
|
23
25
|
description?: string;
|
|
24
26
|
examples?: string[];
|
|
@@ -1,14 +1,57 @@
|
|
|
1
1
|
export declare function findPackageRoot(): string | undefined;
|
|
2
2
|
export declare function packageVersion(packageRoot: string | undefined): string;
|
|
3
3
|
export declare function gitDirectory(packageRoot: string): string;
|
|
4
|
+
export declare function isFullRevision(value: unknown): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Full checkout HEAD revision (40-lowercase-hex) — the basis for staleness
|
|
7
|
+
* comparison. Resolution follows Git's own layouts: a detached revision, then
|
|
8
|
+
* loose refs in the worktree git dir, then the shared store reached through
|
|
9
|
+
* `commondir`, then packed-refs in each. Anything that is not a full
|
|
10
|
+
* lowercase hexadecimal revision is rejected.
|
|
11
|
+
*/
|
|
12
|
+
export declare function gitFullRevision(packageRoot: string | undefined): string | undefined;
|
|
4
13
|
export declare function gitRevision(packageRoot: string | undefined): string | undefined;
|
|
14
|
+
export type CliLoadSource = "source" | "dist";
|
|
15
|
+
export type CliRuntime = "bun";
|
|
16
|
+
export declare function detectLoadSource(modulePath?: string): CliLoadSource;
|
|
17
|
+
export declare function buildMetadataRevision(packageRoot: string | undefined): string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Whether compiled CLI output predates any source input. The build metadata
|
|
20
|
+
* file is written after `tsc`, so its mtime is the build-completion marker.
|
|
21
|
+
* Installed packages omit `src/`; without both sides there is no drift claim.
|
|
22
|
+
*/
|
|
23
|
+
export declare function sourceTreeNewerThanBuild(packageRoot: string | undefined): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* CLI provenance payload. `source` distinguishes a source checkout run from a
|
|
26
|
+
* published dist run; `runtime` names the executing runtime. `buildRevision`
|
|
27
|
+
* is only ever claimed by a dist build that carries build metadata — source
|
|
28
|
+
* execution never claims one. `staleBuild` is true only when the running dist
|
|
29
|
+
* build revision differs from the checkout or a source input is newer than the
|
|
30
|
+
* build-completion marker.
|
|
31
|
+
*/
|
|
32
|
+
export interface CliVersionPayload {
|
|
33
|
+
version: string;
|
|
34
|
+
gitRevision: string | null;
|
|
35
|
+
source: CliLoadSource;
|
|
36
|
+
runtime: CliRuntime;
|
|
37
|
+
buildRevision: string | null;
|
|
38
|
+
staleBuild: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** Bun is the only supported runtime; the field is retained for payload stability. */
|
|
41
|
+
export declare function detectRuntime(): CliRuntime;
|
|
42
|
+
export declare function buildVersionPayload(input: {
|
|
43
|
+
packageVersion: string;
|
|
44
|
+
checkoutRevision: string | undefined;
|
|
45
|
+
checkoutFullRevision: string | undefined;
|
|
46
|
+
loadSource: string | undefined;
|
|
47
|
+
buildRevision: string | undefined;
|
|
48
|
+
sourceNewerThanBuild?: boolean;
|
|
49
|
+
runtime: CliRuntime;
|
|
50
|
+
}): CliVersionPayload;
|
|
5
51
|
export declare const PACKAGE_ROOT: string | undefined;
|
|
6
52
|
export declare const CLI_VERSION: string;
|
|
7
53
|
export declare const CLI_GIT_REVISION: string | undefined;
|
|
8
54
|
export declare const AGENT_CONTRACT_VERSION = 2;
|
|
9
55
|
export declare const JSON_SCHEMA_DRAFT = "https://json-schema.org/draft/2020-12/schema";
|
|
10
56
|
export declare const AGENT_CONTRACT_SCHEMA_ID = "https://clssck.github.io/dataiku-sdk/schemas/agent-contract-v2.json";
|
|
11
|
-
export declare function cliVersionResult():
|
|
12
|
-
version: string;
|
|
13
|
-
gitRevision: string | null;
|
|
14
|
-
};
|
|
57
|
+
export declare function cliVersionResult(): CliVersionPayload;
|
package/dist/src/cli/version.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, } from "node:fs";
|
|
1
|
+
import { readdirSync, readFileSync, statSync, } from "node:fs";
|
|
2
2
|
import { dirname, resolve, } from "node:path";
|
|
3
3
|
import { fileURLToPath, } from "node:url";
|
|
4
4
|
export function findPackageRoot() {
|
|
@@ -36,22 +36,175 @@ export function gitDirectory(packageRoot) {
|
|
|
36
36
|
}
|
|
37
37
|
return resolve(packageRoot, ".git");
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* A full lowercase hexadecimal revision is the only provenance claim trusted.
|
|
41
|
+
* Git is never invoked from runtime version reporting; revisions are resolved
|
|
42
|
+
* directly from the filesystem layouts Git documents.
|
|
43
|
+
*/
|
|
44
|
+
const FULL_REVISION_PATTERN = /^[0-9a-f]{40}$/;
|
|
45
|
+
export function isFullRevision(value) {
|
|
46
|
+
return typeof value === "string" && FULL_REVISION_PATTERN.test(value);
|
|
47
|
+
}
|
|
48
|
+
function readGitFile(base, relative) {
|
|
49
|
+
try {
|
|
50
|
+
return readFileSync(resolve(base, relative), "utf-8").trim();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function isSafeRefName(ref) {
|
|
57
|
+
return (ref.length > 0
|
|
58
|
+
&& !ref.startsWith("/")
|
|
59
|
+
&& !ref.endsWith("/")
|
|
60
|
+
&& !ref.includes("\\")
|
|
61
|
+
&& !ref.split("/").includes(".."));
|
|
62
|
+
}
|
|
63
|
+
/** Resolve an entry from packed-refs; peeled `^` lines never match a ref. */
|
|
64
|
+
function packedRefFile(base, ref) {
|
|
65
|
+
let contents;
|
|
66
|
+
try {
|
|
67
|
+
contents = readFileSync(resolve(base, "packed-refs"), "utf-8");
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
for (const line of contents.split("\n")) {
|
|
73
|
+
const trimmed = line.trim();
|
|
74
|
+
if (trimmed.length === 0 || trimmed.startsWith("#") || trimmed.startsWith("^"))
|
|
75
|
+
continue;
|
|
76
|
+
const splitAt = trimmed.indexOf(" ");
|
|
77
|
+
if (splitAt <= 0)
|
|
78
|
+
continue;
|
|
79
|
+
const hash = trimmed.slice(0, splitAt);
|
|
80
|
+
if (isFullRevision(hash) && trimmed.slice(splitAt + 1).trim() === ref)
|
|
81
|
+
return hash;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The shared git dir behind a linked worktree or submodule checkout: the
|
|
87
|
+
* `commondir` file inside the worktree's git dir, resolved relative to that
|
|
88
|
+
* dir. Normal checkouts (no `commondir` file) share their own git dir.
|
|
89
|
+
*/
|
|
90
|
+
function gitCommonDirectory(gitDir) {
|
|
91
|
+
try {
|
|
92
|
+
const commondir = readFileSync(resolve(gitDir, "commondir"), "utf-8").trim();
|
|
93
|
+
return commondir.length > 0 ? resolve(gitDir, commondir) : undefined;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Full checkout HEAD revision (40-lowercase-hex) — the basis for staleness
|
|
101
|
+
* comparison. Resolution follows Git's own layouts: a detached revision, then
|
|
102
|
+
* loose refs in the worktree git dir, then the shared store reached through
|
|
103
|
+
* `commondir`, then packed-refs in each. Anything that is not a full
|
|
104
|
+
* lowercase hexadecimal revision is rejected.
|
|
105
|
+
*/
|
|
106
|
+
export function gitFullRevision(packageRoot) {
|
|
107
|
+
if (!packageRoot)
|
|
108
|
+
return undefined;
|
|
109
|
+
const gitDir = gitDirectory(packageRoot);
|
|
110
|
+
const head = readGitFile(gitDir, "HEAD");
|
|
111
|
+
if (!head)
|
|
112
|
+
return undefined;
|
|
113
|
+
if (isFullRevision(head))
|
|
114
|
+
return head;
|
|
115
|
+
if (!head.startsWith("ref:"))
|
|
116
|
+
return undefined;
|
|
117
|
+
const ref = head.slice("ref:".length).trim();
|
|
118
|
+
if (!isSafeRefName(ref))
|
|
119
|
+
return undefined;
|
|
120
|
+
for (const base of [gitDir, gitCommonDirectory(gitDir),]) {
|
|
121
|
+
if (!base)
|
|
122
|
+
continue;
|
|
123
|
+
const loose = readGitFile(base, ref);
|
|
124
|
+
if (loose && isFullRevision(loose))
|
|
125
|
+
return loose;
|
|
126
|
+
const packed = packedRefFile(base, ref);
|
|
127
|
+
if (packed)
|
|
128
|
+
return packed;
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
39
132
|
export function gitRevision(packageRoot) {
|
|
133
|
+
const full = gitFullRevision(packageRoot);
|
|
134
|
+
return full ? full.slice(0, 7) : undefined;
|
|
135
|
+
}
|
|
136
|
+
export function detectLoadSource(modulePath = fileURLToPath(import.meta.url)) {
|
|
137
|
+
return modulePath.replaceAll("\\", "/").includes("/dist/") ? "dist" : "source";
|
|
138
|
+
}
|
|
139
|
+
export function buildMetadataRevision(packageRoot) {
|
|
40
140
|
if (!packageRoot)
|
|
41
141
|
return undefined;
|
|
42
142
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const full = readFileSync(resolve(gitDir, ref), "utf-8").trim();
|
|
49
|
-
return full.slice(0, 7);
|
|
143
|
+
const metadata = JSON.parse(readFileSync(resolve(packageRoot, "dist", "build-metadata.json"), "utf-8"));
|
|
144
|
+
return typeof metadata.buildRevision === "string"
|
|
145
|
+
&& isFullRevision(metadata.buildRevision)
|
|
146
|
+
? metadata.buildRevision
|
|
147
|
+
: undefined;
|
|
50
148
|
}
|
|
51
149
|
catch {
|
|
52
150
|
return undefined;
|
|
53
151
|
}
|
|
54
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Whether compiled CLI output predates any source input. The build metadata
|
|
155
|
+
* file is written after `tsc`, so its mtime is the build-completion marker.
|
|
156
|
+
* Installed packages omit `src/`; without both sides there is no drift claim.
|
|
157
|
+
*/
|
|
158
|
+
export function sourceTreeNewerThanBuild(packageRoot) {
|
|
159
|
+
if (!packageRoot)
|
|
160
|
+
return false;
|
|
161
|
+
try {
|
|
162
|
+
const buildMtimeMs = statSync(resolve(packageRoot, "dist", "build-metadata.json")).mtimeMs;
|
|
163
|
+
const pending = [resolve(packageRoot, "src"),];
|
|
164
|
+
while (pending.length > 0) {
|
|
165
|
+
const path = pending.pop();
|
|
166
|
+
if (!path)
|
|
167
|
+
break;
|
|
168
|
+
const stat = statSync(path);
|
|
169
|
+
if (stat.mtimeMs > buildMtimeMs)
|
|
170
|
+
return true;
|
|
171
|
+
if (!stat.isDirectory())
|
|
172
|
+
continue;
|
|
173
|
+
for (const entry of readdirSync(path, { withFileTypes: true, })) {
|
|
174
|
+
if (entry.isDirectory() || entry.isFile()) {
|
|
175
|
+
pending.push(resolve(path, entry.name));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Bun is the only supported runtime; the field is retained for payload stability. */
|
|
186
|
+
export function detectRuntime() {
|
|
187
|
+
return "bun";
|
|
188
|
+
}
|
|
189
|
+
export function buildVersionPayload(input) {
|
|
190
|
+
const isDist = input.loadSource === "dist";
|
|
191
|
+
// Source execution never claims a build revision, even if the environment
|
|
192
|
+
// happens to carry one.
|
|
193
|
+
const buildRevision = isDist && input.buildRevision ? input.buildRevision : null;
|
|
194
|
+
const staleBuild = isDist
|
|
195
|
+
&& ((buildRevision !== null
|
|
196
|
+
&& input.checkoutFullRevision !== undefined
|
|
197
|
+
&& input.checkoutFullRevision !== buildRevision)
|
|
198
|
+
|| input.sourceNewerThanBuild === true);
|
|
199
|
+
return {
|
|
200
|
+
version: input.packageVersion,
|
|
201
|
+
gitRevision: input.checkoutRevision ?? null,
|
|
202
|
+
source: isDist ? "dist" : "source",
|
|
203
|
+
runtime: input.runtime,
|
|
204
|
+
buildRevision,
|
|
205
|
+
staleBuild,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
55
208
|
export const PACKAGE_ROOT = findPackageRoot();
|
|
56
209
|
export const CLI_VERSION = packageVersion(PACKAGE_ROOT);
|
|
57
210
|
export const CLI_GIT_REVISION = gitRevision(PACKAGE_ROOT);
|
|
@@ -59,5 +212,19 @@ export const AGENT_CONTRACT_VERSION = 2;
|
|
|
59
212
|
export const JSON_SCHEMA_DRAFT = "https://json-schema.org/draft/2020-12/schema";
|
|
60
213
|
export const AGENT_CONTRACT_SCHEMA_ID = "https://clssck.github.io/dataiku-sdk/schemas/agent-contract-v2.json";
|
|
61
214
|
export function cliVersionResult() {
|
|
62
|
-
|
|
215
|
+
const loadSource = process.env["DSS_LOAD_SOURCE"] ?? detectLoadSource();
|
|
216
|
+
const envBuildRevision = process.env["DSS_BUILD_REVISION"];
|
|
217
|
+
return buildVersionPayload({
|
|
218
|
+
packageVersion: CLI_VERSION,
|
|
219
|
+
checkoutRevision: CLI_GIT_REVISION,
|
|
220
|
+
checkoutFullRevision: gitFullRevision(PACKAGE_ROOT),
|
|
221
|
+
loadSource,
|
|
222
|
+
buildRevision: isFullRevision(envBuildRevision)
|
|
223
|
+
? envBuildRevision
|
|
224
|
+
: (loadSource === "dist" ? buildMetadataRevision(PACKAGE_ROOT) : undefined),
|
|
225
|
+
sourceNewerThanBuild: loadSource === "dist"
|
|
226
|
+
? sourceTreeNewerThanBuild(PACKAGE_ROOT)
|
|
227
|
+
: false,
|
|
228
|
+
runtime: detectRuntime(),
|
|
229
|
+
});
|
|
63
230
|
}
|