@dataparade/cli 0.0.6 → 0.0.7
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 +3 -2
- package/dist/package.json +1 -1
- package/dist/src/cli.js +65 -36
- package/dist/src/config/env.js +7 -2
- package/dist/src/config/load-cli-env.d.ts +2 -0
- package/dist/src/config/load-cli-env.js +11 -2
- package/dist/src/config/redact.js +3 -0
- package/dist/src/config/validate-scan-ai.js +13 -7
- package/dist/src/core/pipeline/ai-orchestrator-options.js +8 -4
- package/dist/src/core/pipeline/orchestrator.js +1 -1
- package/dist/src/core/schema/scan-config.schema.d.ts +1 -0
- package/dist/src/core/schema/scan-config.schema.js +1 -0
- package/dist/src/core/types/config.d.ts +5 -0
- package/dist/src/platform-api/anonymous-ai-session-client.d.ts +14 -0
- package/dist/src/platform-api/anonymous-ai-session-client.js +40 -0
- package/dist/src/platform-api/upload-client.d.ts +10 -3
- package/dist/src/platform-api/upload-client.js +35 -16
- package/dist/src/platform-api/upload.types.d.ts +10 -5
- package/dist/src/upload/build-preview-url.d.ts +1 -0
- package/dist/src/upload/build-preview-url.js +6 -0
- package/dist/src/upload/run-upload.js +20 -4
- package/dist/src/upload/types.d.ts +2 -2
- package/dist/tests/cli/cli.spec.js +17 -3
- package/dist/tests/unit/cli/scan-command-ai-flags.spec.js +19 -0
- package/dist/tests/unit/cli/scan-command-e2e-monorepo-app-plus-terraform.spec.js +12 -8
- package/dist/tests/unit/cli/scan-command-e2e-monorepo-front-back-sections.spec.js +10 -1
- package/dist/tests/unit/cli/scan-command-e2e-python-basic.spec.js +10 -1
- package/dist/tests/unit/cli/scan-command-e2e-terraform-basic.spec.js +12 -8
- package/dist/tests/unit/cli/scan-command-e2e-ts-sample.spec.js +10 -1
- package/dist/tests/unit/cli/scan-command-e2e-ts-sentry-route-env.spec.js +1 -1
- package/dist/tests/unit/cli/scan-command-exit-code.spec.js +21 -0
- package/dist/tests/unit/cli/scan-quota-flow.spec.js +1 -1
- package/dist/tests/unit/config/load-cli-env.spec.d.ts +1 -0
- package/dist/tests/unit/config/load-cli-env.spec.js +52 -0
- package/dist/tests/unit/config/redact.spec.js +1 -1
- package/dist/tests/unit/config/resolve-config.spec.js +1 -1
- package/dist/tests/unit/config/validate-scan-ai.spec.js +25 -0
- package/dist/tests/unit/core/ai-orchestrator-options-anon.spec.d.ts +1 -0
- package/dist/tests/unit/core/ai-orchestrator-options-anon.spec.js +29 -0
- package/dist/tests/unit/core/dependency-manifest-performance-budgets.spec.js +1 -1
- package/dist/tests/unit/core/graph-mapping.spec.js +1 -1
- package/dist/tests/unit/core/invariant-tests-dedupe-determinism.spec.js +1 -1
- package/dist/tests/unit/core/no-source-code-leakage.spec.js +1 -1
- package/dist/tests/unit/core/orchestrator.spec.js +12 -12
- package/dist/tests/unit/core/output-json.spec.js +2 -2
- package/dist/tests/unit/core/parsing-error-handling-nonfatal.spec.js +1 -1
- package/dist/tests/unit/core/scan-result-language-stats.spec.js +1 -1
- package/dist/tests/unit/core/structural-scan.spec.js +1 -1
- package/dist/tests/unit/core/terraform-json-overlay.spec.js +1 -1
- package/dist/tests/unit/platform-api/anonymous-ai-session-client.spec.d.ts +1 -0
- package/dist/tests/unit/platform-api/anonymous-ai-session-client.spec.js +35 -0
- package/dist/tests/unit/upload/build-preview-url.spec.js +16 -0
- package/dist/tests/unit/upload/run-upload.spec.d.ts +1 -0
- package/dist/tests/unit/upload/run-upload.spec.js +43 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,10 +38,11 @@ List commands and options: `npx @dataparade/cli --help` and `npx @dataparade/cli
|
|
|
38
38
|
|
|
39
39
|
## Upload to dashboard
|
|
40
40
|
|
|
41
|
-
After a scan, the CLI **auto-uploads** `dataflow.json`
|
|
41
|
+
After a scan, the CLI **auto-uploads** `dataflow.json` by default (unless you opt out). This creates an **import preview draft** in the web app — not a finished assessment.
|
|
42
42
|
|
|
43
|
+
- **With a workspace API key** (`DATAPARADE_WORKSPACE_API_KEY` from **Workspace → Access keys**): prints a dashboard link (`?importDraft=`) for that workspace.
|
|
44
|
+
- **Without a workspace key**: prints a sign-up link (`/preview/cli/<token>`). Create an account to open **Preview & Edit** on your dashboard.
|
|
43
45
|
- Opt out: `--skip-auto-upload` or `DATAPARADE_SKIP_AUTO_UPLOAD=true`
|
|
44
|
-
- Without a workspace key, the scan still succeeds; run `upload` later
|
|
45
46
|
- Upload alone does **not** consume scan quota
|
|
46
47
|
|
|
47
48
|
```bash
|
package/dist/package.json
CHANGED
package/dist/src/cli.js
CHANGED
|
@@ -130,7 +130,8 @@ function createProgram() {
|
|
|
130
130
|
.option("--no-terraform-stack-section-auto", "Do not infer terraformStackSectionPathDepth from .tf layout when depth is unset")
|
|
131
131
|
.option("--monorepo-package-section-path-depth <n>", "Workspace package section depth (max 3): primary packages and rollup use N POSIX path segments (e.g. 3 for packages/twenty-apps/hello)")
|
|
132
132
|
.option("--no-monorepo-package-section-auto", "Do not infer monorepoPackageSectionPathDepth from package.json layout when depth is unset")
|
|
133
|
-
.option("--ai-inference", "Enable post-scan AI inference
|
|
133
|
+
.option("--ai-inference", "Enable post-scan AI inference (default: on; SCAN_AI_INFERENCE=false also disables)")
|
|
134
|
+
.option("--no-ai-inference", "Disable post-scan AI inference")
|
|
134
135
|
.option("--ai-provider <provider>", `AI provider: ${types_1.AI_PROVIDER_IDS.join("|")}`)
|
|
135
136
|
.option("--ai-model <model>", "Model name for AI inference")
|
|
136
137
|
.option("--ai-endpoint <url>", "Override provider endpoint URL")
|
|
@@ -265,14 +266,53 @@ function createProgram() {
|
|
|
265
266
|
config.cliQuotaJobId = preflight.jobId;
|
|
266
267
|
if (config.enableAiInference &&
|
|
267
268
|
preflight.suggestedAiBudgetTokens > 0) {
|
|
268
|
-
|
|
269
|
+
// suggestedAiBudgetTokens is a server ceiling (remaining quota /
|
|
270
|
+
// per-job cap), not a spend target. Never raise the CLI working
|
|
271
|
+
// budget above the configured default (or user override).
|
|
272
|
+
config.aiBudgetTokens = Math.min(config.aiBudgetTokens ?? 12_000, preflight.suggestedAiBudgetTokens);
|
|
269
273
|
}
|
|
270
274
|
if (preflight.aiDelivery === "platform_proxy") {
|
|
271
275
|
config.aiMode = "platform";
|
|
272
276
|
}
|
|
273
277
|
if (isInteractive) {
|
|
274
278
|
// eslint-disable-next-line no-console
|
|
275
|
-
console.log(`[scan] quota: scans_remaining=${preflight.scansRemaining} ai_tokens_remaining=${preflight.aiTokensRemaining} job_id=${preflight.jobId}`);
|
|
279
|
+
console.log(`[scan] quota: scans_remaining=${preflight.scansRemaining} ai_tokens_remaining=${preflight.aiTokensRemaining} job_id=${preflight.jobId} working_budget=${config.aiBudgetTokens ?? 0}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else if (!workspaceApiKey &&
|
|
283
|
+
!usesByok &&
|
|
284
|
+
config.enableAiInference) {
|
|
285
|
+
try {
|
|
286
|
+
const { cliAnonymousAiSession } = await Promise.resolve().then(() => __importStar(require("./platform-api/anonymous-ai-session-client")));
|
|
287
|
+
const session = await cliAnonymousAiSession({
|
|
288
|
+
projectName: config.projectName,
|
|
289
|
+
});
|
|
290
|
+
cliQuotaJobId = session.jobId;
|
|
291
|
+
config.cliQuotaJobId = session.jobId;
|
|
292
|
+
config.anonSessionToken = session.sessionToken;
|
|
293
|
+
if (session.suggestedAiBudgetTokens > 0) {
|
|
294
|
+
// Anon suggested value is the hard per-job ceiling (default 100k).
|
|
295
|
+
// Applying it as the working budget caused scans to burn ~100k tokens.
|
|
296
|
+
config.aiBudgetTokens = Math.min(config.aiBudgetTokens ?? 12_000, session.suggestedAiBudgetTokens);
|
|
297
|
+
}
|
|
298
|
+
if (session.aiDelivery === "platform_proxy") {
|
|
299
|
+
config.aiMode = "platform";
|
|
300
|
+
}
|
|
301
|
+
if (isInteractive) {
|
|
302
|
+
// eslint-disable-next-line no-console
|
|
303
|
+
console.log(`[scan] anonymous platform AI: job_id=${session.jobId} working_budget=${config.aiBudgetTokens ?? 0} server_cap=${session.suggestedAiBudgetTokens} (claim debits actual tokens used)`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch (anonSessionError) {
|
|
307
|
+
const { CliAnonymousIpLimitError } = await Promise.resolve().then(() => __importStar(require("./platform-api/anonymous-ai-session-client")));
|
|
308
|
+
const message = anonSessionError instanceof Error
|
|
309
|
+
? anonSessionError.message
|
|
310
|
+
: "Anonymous AI session failed.";
|
|
311
|
+
// eslint-disable-next-line no-console
|
|
312
|
+
console.error(`[scan] ${message}`);
|
|
313
|
+
process.exitCode =
|
|
314
|
+
anonSessionError instanceof CliAnonymousIpLimitError ? 2 : 1;
|
|
315
|
+
return;
|
|
276
316
|
}
|
|
277
317
|
}
|
|
278
318
|
const configValidation = (0, scan_config_schema_1.validateScanConfiguration)(config);
|
|
@@ -310,10 +350,12 @@ function createProgram() {
|
|
|
310
350
|
const s = scanResult.aiInferenceSummary;
|
|
311
351
|
// eslint-disable-next-line no-console
|
|
312
352
|
console.log(`[scan] llm-inference summary: candidates=${s.candidatesConsidered} proposals=${s.proposalsGenerated} (provider=${s.proposalsGeneratedProvider}) applied=${s.proposalsApplied} (provider=${s.proposalsAppliedProvider}) rejected=${s.proposalsRejected} (${s.aiProvider}/${s.aiModel})`);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
353
|
+
// Always show real provider token usage (budget_cap is only a ceiling, not cost).
|
|
354
|
+
// eslint-disable-next-line no-console
|
|
355
|
+
console.log(`[scan] ai-usage: provider_calls=${s.providerCalls} tokens_in=${s.inputTokens} tokens_out=${s.outputTokens} tokens_total=${s.totalTokens}` +
|
|
356
|
+
(typeof s.estimatedCostUsd === "number"
|
|
357
|
+
? ` estimated_cost_usd=${formatUsd(s.estimatedCostUsd)}`
|
|
358
|
+
: ""));
|
|
317
359
|
if (options.aiVerbose && scanResult.aiInferenceProposalDetails) {
|
|
318
360
|
printAiInferenceVerbose(scanResult.aiInferenceProposalDetails);
|
|
319
361
|
}
|
|
@@ -387,30 +429,23 @@ function createProgram() {
|
|
|
387
429
|
const skipAutoUpload = Boolean(options.skipAutoUpload) ||
|
|
388
430
|
(0, upload_env_1.resolveSkipAutoUpload)(process.env);
|
|
389
431
|
if (!skipAutoUpload) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
432
|
+
try {
|
|
433
|
+
const { runDataflowUpload } = await Promise.resolve().then(() => __importStar(require("./upload/run-upload")));
|
|
434
|
+
const dataflowWrapper = (0, json_1.buildDataflowWrapper)(scanResult, diagramGraph, { projectName: resolvedProjectName });
|
|
435
|
+
await runDataflowUpload({
|
|
436
|
+
apiKey: workspaceApiKey,
|
|
437
|
+
dataflow: dataflowWrapper,
|
|
438
|
+
projectName: resolvedProjectName,
|
|
439
|
+
scanJobId: cliQuotaJobId,
|
|
440
|
+
logPrefix: "[scan]",
|
|
441
|
+
});
|
|
394
442
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
dataflow: dataflowWrapper,
|
|
402
|
-
projectName: resolvedProjectName,
|
|
403
|
-
scanJobId: cliQuotaJobId,
|
|
404
|
-
logPrefix: "[scan]",
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
catch (uploadError) {
|
|
408
|
-
const uploadMessage = uploadError instanceof Error
|
|
409
|
-
? uploadError.message
|
|
410
|
-
: "Unknown upload error.";
|
|
411
|
-
// eslint-disable-next-line no-console
|
|
412
|
-
console.error(`[scan] Auto-upload failed: ${uploadMessage}`);
|
|
413
|
-
}
|
|
443
|
+
catch (uploadError) {
|
|
444
|
+
const uploadMessage = uploadError instanceof Error
|
|
445
|
+
? uploadError.message
|
|
446
|
+
: "Unknown upload error.";
|
|
447
|
+
// eslint-disable-next-line no-console
|
|
448
|
+
console.error(`[scan] Auto-upload failed: ${uploadMessage}`);
|
|
414
449
|
}
|
|
415
450
|
}
|
|
416
451
|
}
|
|
@@ -498,12 +533,6 @@ function createProgram() {
|
|
|
498
533
|
const apiKey = options.workspaceApiKey?.trim() ||
|
|
499
534
|
options.apiKey?.trim() ||
|
|
500
535
|
(0, scan_env_1.resolveWorkspaceApiKey)(process.env);
|
|
501
|
-
if (!apiKey) {
|
|
502
|
-
// eslint-disable-next-line no-console
|
|
503
|
-
console.error("[upload] error: workspace API key is required (DATAPARADE_WORKSPACE_API_KEY or --workspace-api-key)");
|
|
504
|
-
process.exitCode = 1;
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
536
|
const filePath = path_1.default.resolve(process.cwd(), file || "dataflow.json");
|
|
508
537
|
let dataflow;
|
|
509
538
|
try {
|
package/dist/src/config/env.js
CHANGED
|
@@ -22,10 +22,15 @@ function parseBoolean(value) {
|
|
|
22
22
|
if (value == null)
|
|
23
23
|
return undefined;
|
|
24
24
|
const normalized = value.trim().toLowerCase();
|
|
25
|
-
if (normalized === "true")
|
|
25
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") {
|
|
26
26
|
return true;
|
|
27
|
-
|
|
27
|
+
}
|
|
28
|
+
if (normalized === "false" ||
|
|
29
|
+
normalized === "0" ||
|
|
30
|
+
normalized === "no" ||
|
|
31
|
+
normalized === "off") {
|
|
28
32
|
return false;
|
|
33
|
+
}
|
|
29
34
|
return undefined;
|
|
30
35
|
}
|
|
31
36
|
function loadCliConfigEnv(env) {
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.findCliPackageRoot = findCliPackageRoot;
|
|
6
7
|
exports.loadCliDotenv = loadCliDotenv;
|
|
7
8
|
/**
|
|
8
9
|
* Loads `cli/.env` when not running under Jest.
|
|
@@ -16,9 +17,12 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
16
17
|
const dotenv_1 = require("dotenv");
|
|
17
18
|
const CLI_PACKAGE_NAME = "@dataparade/cli";
|
|
18
19
|
const tried = { current: false };
|
|
20
|
+
/** @internal Exported for unit tests. */
|
|
19
21
|
function findCliPackageRoot(startDir) {
|
|
20
22
|
let dir = node_path_1.default.resolve(startDir);
|
|
21
23
|
const { root } = node_path_1.default.parse(dir);
|
|
24
|
+
/** Fallback when tsc emits package.json under dist/ but no outer root is found. */
|
|
25
|
+
let distPackageRoot;
|
|
22
26
|
while (dir !== root) {
|
|
23
27
|
const pkgPath = node_path_1.default.join(dir, "package.json");
|
|
24
28
|
if (node_fs_1.default.existsSync(pkgPath)) {
|
|
@@ -26,7 +30,12 @@ function findCliPackageRoot(startDir) {
|
|
|
26
30
|
const raw = node_fs_1.default.readFileSync(pkgPath, "utf8");
|
|
27
31
|
const pkg = JSON.parse(raw);
|
|
28
32
|
if (pkg.name === CLI_PACKAGE_NAME) {
|
|
29
|
-
|
|
33
|
+
if (node_path_1.default.basename(dir) === "dist") {
|
|
34
|
+
distPackageRoot = dir;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
30
39
|
}
|
|
31
40
|
}
|
|
32
41
|
catch {
|
|
@@ -35,7 +44,7 @@ function findCliPackageRoot(startDir) {
|
|
|
35
44
|
}
|
|
36
45
|
dir = node_path_1.default.dirname(dir);
|
|
37
46
|
}
|
|
38
|
-
return
|
|
47
|
+
return distPackageRoot;
|
|
39
48
|
}
|
|
40
49
|
function loadCliDotenv() {
|
|
41
50
|
if (tried.current)
|
|
@@ -13,15 +13,20 @@ function validateAiInferenceCredentials(config) {
|
|
|
13
13
|
Boolean(config.aiModel?.trim()) &&
|
|
14
14
|
Boolean(config.aiApiKey?.trim());
|
|
15
15
|
const hasWorkspace = Boolean(config.workspaceApiKey?.trim());
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const hasAnonSession = Boolean(config.anonSessionToken?.trim());
|
|
17
|
+
if (hasByok && (hasWorkspace || hasAnonSession)) {
|
|
18
|
+
errors.push("ai-inference: set either SCAN_BYOK_* (your LLM provider) or platform AI (DATAPARADE_WORKSPACE_API_KEY or anonymous session), not both");
|
|
18
19
|
return errors;
|
|
19
20
|
}
|
|
20
|
-
if (
|
|
21
|
-
errors.push("ai-inference:
|
|
21
|
+
if (hasWorkspace && hasAnonSession) {
|
|
22
|
+
errors.push("ai-inference: set either DATAPARADE_WORKSPACE_API_KEY or an anonymous AI session, not both");
|
|
23
|
+
return errors;
|
|
24
|
+
}
|
|
25
|
+
if (!hasByok && !hasWorkspace && !hasAnonSession) {
|
|
26
|
+
errors.push("ai-inference: LLM inference requires SCAN_BYOK_PROVIDER, SCAN_BYOK_MODEL, and SCAN_BYOK_API_KEY, DATAPARADE_WORKSPACE_API_KEY for platform AI, or an anonymous platform AI session");
|
|
22
27
|
}
|
|
23
|
-
if (hasWorkspace && !config.cliQuotaJobId) {
|
|
24
|
-
errors.push("ai-inference: platform AI requires a quota
|
|
28
|
+
if ((hasWorkspace || hasAnonSession) && !config.cliQuotaJobId) {
|
|
29
|
+
errors.push("ai-inference: platform AI requires a quota/session job id (preflight or anonymous-session must succeed)");
|
|
25
30
|
}
|
|
26
31
|
return errors;
|
|
27
32
|
}
|
|
@@ -32,7 +37,8 @@ function resolveAiMode(config) {
|
|
|
32
37
|
return "hosted_worker";
|
|
33
38
|
if (config.aiApiKey?.trim())
|
|
34
39
|
return "byok";
|
|
35
|
-
if (config.workspaceApiKey?.trim())
|
|
40
|
+
if (config.workspaceApiKey?.trim() || config.anonSessionToken?.trim()) {
|
|
36
41
|
return "platform";
|
|
42
|
+
}
|
|
37
43
|
return "none";
|
|
38
44
|
}
|
|
@@ -8,7 +8,9 @@ function buildAgentOrchestratorOptions(config, opts) {
|
|
|
8
8
|
const platformProvider = process.env.SCAN_WORKER_LLM_PROVIDER?.trim() ||
|
|
9
9
|
"openai";
|
|
10
10
|
const platformModel = process.env.SCAN_WORKER_LLM_MODEL?.trim() ||
|
|
11
|
-
config.aiModel?.trim()
|
|
11
|
+
(config.aiModel?.trim() && config.aiModel.trim() !== "heuristic"
|
|
12
|
+
? config.aiModel.trim()
|
|
13
|
+
: undefined) ||
|
|
12
14
|
"gpt-4o-mini";
|
|
13
15
|
const base = {
|
|
14
16
|
provider: aiMode === "platform"
|
|
@@ -29,11 +31,13 @@ function buildAgentOrchestratorOptions(config, opts) {
|
|
|
29
31
|
skipStructuralHeuristics: opts.skipStructuralHeuristics,
|
|
30
32
|
};
|
|
31
33
|
if (aiMode === "platform" &&
|
|
32
|
-
config.
|
|
33
|
-
config.
|
|
34
|
+
config.cliQuotaJobId?.trim() &&
|
|
35
|
+
(config.workspaceApiKey?.trim() || config.anonSessionToken?.trim())) {
|
|
34
36
|
base.platformProxy = {
|
|
35
37
|
apiBaseUrl: config.platformApiBaseUrl ?? (0, dataparade_api_base_url_1.getDataparadeApiBaseUrl)(),
|
|
36
|
-
workspaceApiKey: config.workspaceApiKey
|
|
38
|
+
workspaceApiKey: (config.workspaceApiKey?.trim() ||
|
|
39
|
+
config.anonSessionToken?.trim() ||
|
|
40
|
+
""),
|
|
37
41
|
jobId: config.cliQuotaJobId.trim(),
|
|
38
42
|
};
|
|
39
43
|
base.apiKey = undefined;
|
|
@@ -27,7 +27,7 @@ function createDefaultScanConfiguration(overrides = {}) {
|
|
|
27
27
|
languages: undefined,
|
|
28
28
|
minimumConfidence: 0.5,
|
|
29
29
|
deepAnalysis: false,
|
|
30
|
-
enableAiInference:
|
|
30
|
+
enableAiInference: true,
|
|
31
31
|
aiProvider: "mock",
|
|
32
32
|
aiModel: "heuristic",
|
|
33
33
|
aiEndpoint: undefined,
|
|
@@ -48,6 +48,7 @@ export declare const scanConfigurationSchema: z.ZodObject<{
|
|
|
48
48
|
}>>;
|
|
49
49
|
aiVerbose: z.ZodOptional<z.ZodBoolean>;
|
|
50
50
|
workspaceApiKey: z.ZodOptional<z.ZodString>;
|
|
51
|
+
anonSessionToken: z.ZodOptional<z.ZodString>;
|
|
51
52
|
aiMode: z.ZodOptional<z.ZodEnum<{
|
|
52
53
|
byok: "byok";
|
|
53
54
|
platform: "platform";
|
|
@@ -45,6 +45,7 @@ exports.scanConfigurationSchema = zod_1.z.object({
|
|
|
45
45
|
aiInferenceScope: zod_1.z.enum(["default", "third_party_only"]).optional(),
|
|
46
46
|
aiVerbose: zod_1.z.boolean().optional(),
|
|
47
47
|
workspaceApiKey: zod_1.z.string().min(1).optional(),
|
|
48
|
+
anonSessionToken: zod_1.z.string().min(1).optional(),
|
|
48
49
|
aiMode: zod_1.z.enum(["byok", "platform", "hosted_worker", "none"]).optional(),
|
|
49
50
|
platformApiBaseUrl: zod_1.z.string().min(1).optional(),
|
|
50
51
|
cliQuotaJobId: zod_1.z.string().min(1).optional(),
|
|
@@ -48,6 +48,11 @@ export interface ScanConfiguration {
|
|
|
48
48
|
aiThirdPartyDataFlowEnabled?: boolean;
|
|
49
49
|
/** DataParade workspace API key (quota + platform LLM proxy). */
|
|
50
50
|
workspaceApiKey?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Anonymous platform AI session token (`dp_anon_…`) from
|
|
53
|
+
* POST /api/scans/cli/ai/anonymous-session. Mutually exclusive with workspaceApiKey.
|
|
54
|
+
*/
|
|
55
|
+
anonSessionToken?: string;
|
|
51
56
|
/** How LLM inference is billed: byok (direct provider) or platform (API proxy). */
|
|
52
57
|
aiMode?: "byok" | "platform" | "hosted_worker" | "none";
|
|
53
58
|
/** Base URL for platform API (preflight / infer / complete). */
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type AnonymousAiSessionResponse = {
|
|
2
|
+
sessionToken: string;
|
|
3
|
+
jobId: string;
|
|
4
|
+
suggestedAiBudgetTokens: number;
|
|
5
|
+
expiresAt: string;
|
|
6
|
+
aiDelivery: "platform_proxy" | "none";
|
|
7
|
+
};
|
|
8
|
+
export declare class CliAnonymousIpLimitError extends Error {
|
|
9
|
+
readonly status = 429;
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
export declare function cliAnonymousAiSession(input?: {
|
|
13
|
+
projectName?: string;
|
|
14
|
+
}): Promise<AnonymousAiSessionResponse>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CliAnonymousIpLimitError = void 0;
|
|
4
|
+
exports.cliAnonymousAiSession = cliAnonymousAiSession;
|
|
5
|
+
const dataparade_api_base_url_1 = require("./dataparade-api-base-url");
|
|
6
|
+
class CliAnonymousIpLimitError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.status = 429;
|
|
10
|
+
this.name = "CliAnonymousIpLimitError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.CliAnonymousIpLimitError = CliAnonymousIpLimitError;
|
|
14
|
+
async function cliAnonymousAiSession(input) {
|
|
15
|
+
const res = await fetch(`${(0, dataparade_api_base_url_1.getDataparadeApiBaseUrl)()}/api/scans/cli/ai/anonymous-session`, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: { "Content-Type": "application/json" },
|
|
18
|
+
body: JSON.stringify({
|
|
19
|
+
projectName: input?.projectName,
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
let message = `Anonymous AI session failed (${res.status})`;
|
|
24
|
+
try {
|
|
25
|
+
const body = (await res.json());
|
|
26
|
+
if (typeof body.message === "string")
|
|
27
|
+
message = body.message;
|
|
28
|
+
else if (Array.isArray(body.message))
|
|
29
|
+
message = body.message.join(", ");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// ignore
|
|
33
|
+
}
|
|
34
|
+
if (res.status === 429) {
|
|
35
|
+
throw new CliAnonymousIpLimitError(message);
|
|
36
|
+
}
|
|
37
|
+
throw new Error(message);
|
|
38
|
+
}
|
|
39
|
+
return (await res.json());
|
|
40
|
+
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
import type { CliUploadPreviewInput, CliUploadPreviewResponse } from "./upload.types";
|
|
2
|
-
export type { CliUploadPreviewInput, CliUploadPreviewResponse } from "./upload.types";
|
|
3
|
-
export declare function
|
|
1
|
+
import type { CliAnonymousUploadPreviewResponse, CliUploadPreviewInput, CliUploadPreviewResponse } from "./upload.types";
|
|
2
|
+
export type { CliAnonymousUploadPreviewResponse, CliUploadPreviewInput, CliUploadPreviewResponse, } from "./upload.types";
|
|
3
|
+
export declare function cliUploadAnonymousPreview(input: {
|
|
4
|
+
dataflow: unknown;
|
|
5
|
+
projectName?: string;
|
|
6
|
+
scanJobId?: string;
|
|
7
|
+
}): Promise<CliAnonymousUploadPreviewResponse>;
|
|
8
|
+
export declare function cliUploadPreview(input: CliUploadPreviewInput & {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
}): Promise<CliUploadPreviewResponse>;
|
|
@@ -1,17 +1,49 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cliUploadAnonymousPreview = cliUploadAnonymousPreview;
|
|
3
4
|
exports.cliUploadPreview = cliUploadPreview;
|
|
4
5
|
const dataparade_api_base_url_1 = require("./dataparade-api-base-url");
|
|
5
|
-
function
|
|
6
|
+
function buildAuthHeaders(apiKey) {
|
|
6
7
|
return {
|
|
7
8
|
Authorization: `Bearer ${apiKey}`,
|
|
8
9
|
"Content-Type": "application/json",
|
|
9
10
|
};
|
|
10
11
|
}
|
|
12
|
+
async function parseUploadError(res) {
|
|
13
|
+
let message = `Upload failed (${res.status})`;
|
|
14
|
+
try {
|
|
15
|
+
const body = (await res.json());
|
|
16
|
+
if (typeof body.message === "string")
|
|
17
|
+
message = body.message;
|
|
18
|
+
else if (Array.isArray(body.message))
|
|
19
|
+
message = body.message.join(", ");
|
|
20
|
+
else if (typeof body.error === "string")
|
|
21
|
+
message = body.error;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// ignore
|
|
25
|
+
}
|
|
26
|
+
throw new Error(message);
|
|
27
|
+
}
|
|
28
|
+
async function cliUploadAnonymousPreview(input) {
|
|
29
|
+
const res = await fetch(`${(0, dataparade_api_base_url_1.getDataparadeApiBaseUrl)()}/api/scans/cli/preview`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
dataflow: input.dataflow,
|
|
34
|
+
projectName: input.projectName,
|
|
35
|
+
scanJobId: input.scanJobId,
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
return await parseUploadError(res);
|
|
40
|
+
}
|
|
41
|
+
return (await res.json());
|
|
42
|
+
}
|
|
11
43
|
async function cliUploadPreview(input) {
|
|
12
44
|
const res = await fetch(`${(0, dataparade_api_base_url_1.getDataparadeApiBaseUrl)()}/api/scans/cli/upload`, {
|
|
13
45
|
method: "POST",
|
|
14
|
-
headers:
|
|
46
|
+
headers: buildAuthHeaders(input.apiKey),
|
|
15
47
|
body: JSON.stringify({
|
|
16
48
|
dataflow: input.dataflow,
|
|
17
49
|
projectName: input.projectName,
|
|
@@ -19,20 +51,7 @@ async function cliUploadPreview(input) {
|
|
|
19
51
|
}),
|
|
20
52
|
});
|
|
21
53
|
if (!res.ok) {
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
const body = (await res.json());
|
|
25
|
-
if (typeof body.message === "string")
|
|
26
|
-
message = body.message;
|
|
27
|
-
else if (Array.isArray(body.message))
|
|
28
|
-
message = body.message.join(", ");
|
|
29
|
-
else if (typeof body.error === "string")
|
|
30
|
-
message = body.error;
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
// ignore
|
|
34
|
-
}
|
|
35
|
-
throw new Error(message);
|
|
54
|
+
return await parseUploadError(res);
|
|
36
55
|
}
|
|
37
56
|
return (await res.json());
|
|
38
57
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
export type CliUploadPreviewResponse = {
|
|
2
|
+
draftId: string;
|
|
3
|
+
projectName?: string | null;
|
|
4
|
+
};
|
|
5
|
+
export type CliAnonymousUploadPreviewResponse = {
|
|
6
|
+
claimToken: string;
|
|
7
|
+
expiresAt: string;
|
|
8
|
+
projectName: string;
|
|
9
|
+
};
|
|
1
10
|
export type CliUploadPreviewInput = {
|
|
2
|
-
apiKey
|
|
11
|
+
apiKey?: string;
|
|
3
12
|
dataflow: unknown;
|
|
4
13
|
projectName?: string;
|
|
5
14
|
scanJobId?: string;
|
|
6
15
|
};
|
|
7
|
-
export type CliUploadPreviewResponse = {
|
|
8
|
-
draftId: string;
|
|
9
|
-
projectName?: string | null;
|
|
10
|
-
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildImportPreviewUrl = buildImportPreviewUrl;
|
|
4
|
+
exports.buildAnonymousCliPreviewUrl = buildAnonymousCliPreviewUrl;
|
|
4
5
|
const dataparade_app_base_url_1 = require("../platform-api/dataparade-app-base-url");
|
|
5
6
|
function buildImportPreviewUrl(draftId) {
|
|
6
7
|
const base = (0, dataparade_app_base_url_1.getDataparadeAppBaseUrl)();
|
|
@@ -8,3 +9,8 @@ function buildImportPreviewUrl(draftId) {
|
|
|
8
9
|
url.searchParams.set("importDraft", draftId);
|
|
9
10
|
return url.toString();
|
|
10
11
|
}
|
|
12
|
+
function buildAnonymousCliPreviewUrl(claimToken) {
|
|
13
|
+
const base = (0, dataparade_app_base_url_1.getDataparadeAppBaseUrl)();
|
|
14
|
+
const encoded = encodeURIComponent(claimToken);
|
|
15
|
+
return `${base}/preview/cli/${encoded}`;
|
|
16
|
+
}
|
|
@@ -5,14 +5,30 @@ const upload_client_1 = require("../platform-api/upload-client");
|
|
|
5
5
|
const build_preview_url_1 = require("./build-preview-url");
|
|
6
6
|
async function runDataflowUpload(input) {
|
|
7
7
|
const prefix = input.logPrefix ?? "[upload]";
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
if (input.apiKey?.trim()) {
|
|
9
|
+
const result = await (0, upload_client_1.cliUploadPreview)({
|
|
10
|
+
apiKey: input.apiKey.trim(),
|
|
11
|
+
dataflow: input.dataflow,
|
|
12
|
+
projectName: input.projectName,
|
|
13
|
+
scanJobId: input.scanJobId,
|
|
14
|
+
});
|
|
15
|
+
const previewUrl = (0, build_preview_url_1.buildImportPreviewUrl)(result.draftId);
|
|
16
|
+
// eslint-disable-next-line no-console
|
|
17
|
+
console.log(`${prefix} Preview ready: ${previewUrl}`);
|
|
18
|
+
return { draftId: result.draftId, previewUrl };
|
|
19
|
+
}
|
|
20
|
+
const result = await (0, upload_client_1.cliUploadAnonymousPreview)({
|
|
10
21
|
dataflow: input.dataflow,
|
|
11
22
|
projectName: input.projectName,
|
|
12
23
|
scanJobId: input.scanJobId,
|
|
13
24
|
});
|
|
14
|
-
const previewUrl = (0, build_preview_url_1.
|
|
25
|
+
const previewUrl = (0, build_preview_url_1.buildAnonymousCliPreviewUrl)(result.claimToken);
|
|
15
26
|
// eslint-disable-next-line no-console
|
|
16
27
|
console.log(`${prefix} Preview ready: ${previewUrl}`);
|
|
17
|
-
|
|
28
|
+
// eslint-disable-next-line no-console
|
|
29
|
+
console.log(`${prefix} Claiming this preview after signup uses 1 scan slot` +
|
|
30
|
+
(input.scanJobId
|
|
31
|
+
? " and any platform AI tokens used during the scan."
|
|
32
|
+
: "."));
|
|
33
|
+
return { previewUrl };
|
|
18
34
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export type RunDataflowUploadInput = {
|
|
2
|
-
apiKey
|
|
2
|
+
apiKey?: string;
|
|
3
3
|
dataflow: unknown;
|
|
4
4
|
projectName?: string;
|
|
5
5
|
scanJobId?: string;
|
|
6
6
|
logPrefix?: string;
|
|
7
7
|
};
|
|
8
8
|
export type RunDataflowUploadResult = {
|
|
9
|
-
draftId
|
|
9
|
+
draftId?: string;
|
|
10
10
|
previewUrl: string;
|
|
11
11
|
};
|
|
@@ -14,7 +14,7 @@ describe("CLI", () => {
|
|
|
14
14
|
}
|
|
15
15
|
});
|
|
16
16
|
it("scan <path> writes a dataflow.json wrapper to the current directory and exits 0", () => {
|
|
17
|
-
const result = (0, child_process_1.spawnSync)("node", [cliDistPath, "scan", "."], {
|
|
17
|
+
const result = (0, child_process_1.spawnSync)("node", [cliDistPath, "scan", ".", "--no-ai-inference", "--skip-auto-upload"], {
|
|
18
18
|
encoding: "utf-8",
|
|
19
19
|
cwd: path_1.default.join(__dirname, "../../"),
|
|
20
20
|
});
|
|
@@ -33,7 +33,13 @@ describe("CLI", () => {
|
|
|
33
33
|
it("scan ../frontend produces a dataflow.json wrapper with at least one node", () => {
|
|
34
34
|
const cliCwd = path_1.default.join(__dirname, "../../");
|
|
35
35
|
const frontendPath = "../frontend";
|
|
36
|
-
const result = (0, child_process_1.spawnSync)("node", [
|
|
36
|
+
const result = (0, child_process_1.spawnSync)("node", [
|
|
37
|
+
cliDistPath,
|
|
38
|
+
"scan",
|
|
39
|
+
frontendPath,
|
|
40
|
+
"--no-ai-inference",
|
|
41
|
+
"--skip-auto-upload",
|
|
42
|
+
], {
|
|
37
43
|
encoding: "utf-8",
|
|
38
44
|
cwd: cliCwd,
|
|
39
45
|
});
|
|
@@ -48,7 +54,15 @@ describe("CLI", () => {
|
|
|
48
54
|
const cliCwd = path_1.default.join(__dirname, "../../");
|
|
49
55
|
const frontendPath = "../frontend";
|
|
50
56
|
const outputFile = "scan-frontend-summary.json";
|
|
51
|
-
const result = (0, child_process_1.spawnSync)("node", [
|
|
57
|
+
const result = (0, child_process_1.spawnSync)("node", [
|
|
58
|
+
cliDistPath,
|
|
59
|
+
"scan",
|
|
60
|
+
frontendPath,
|
|
61
|
+
"--output",
|
|
62
|
+
outputFile,
|
|
63
|
+
"--no-ai-inference",
|
|
64
|
+
"--skip-auto-upload",
|
|
65
|
+
], {
|
|
52
66
|
encoding: "utf-8",
|
|
53
67
|
cwd: cliCwd,
|
|
54
68
|
});
|