@kolatts/pncli 5.0.2 → 5.1.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/dist/cli.js CHANGED
@@ -10,10 +10,11 @@ import {
10
10
  setRepoConfigValue,
11
11
  writeGlobalConfig,
12
12
  writeRepoConfig
13
- } from "./chunk-GRKPFSQD.js";
13
+ } from "./chunk-QHJ2HM5J.js";
14
14
  import {
15
- createHttpClient
16
- } from "./chunk-5Q4VM6GO.js";
15
+ createHttpClient,
16
+ validateAlationAccessToken
17
+ } from "./chunk-FAMJPKO4.js";
17
18
  import {
18
19
  ExitCode,
19
20
  fail,
@@ -6492,6 +6493,20 @@ async function runCredentialChecks(cfg, http) {
6492
6493
  results.figma = categorize(err);
6493
6494
  }
6494
6495
  }
6496
+ if (!cfg.alation.refreshToken) {
6497
+ results.alation = { status: "blank", message: "not configured" };
6498
+ } else if (!cfg.alation.baseUrl) {
6499
+ results.alation = { status: "error", message: "baseUrl not configured" };
6500
+ } else if (cfg.alation.userId === void 0 || cfg.alation.userId === "") {
6501
+ results.alation = { status: "error", message: "userId not configured" };
6502
+ } else {
6503
+ try {
6504
+ await validateAlationAccessToken(cfg);
6505
+ results.alation = { status: "valid", message: "ok" };
6506
+ } catch (err) {
6507
+ results.alation = categorize(err);
6508
+ }
6509
+ }
6495
6510
  return results;
6496
6511
  }
6497
6512
 
@@ -8356,6 +8371,16 @@ function registerConfigCommands(program2) {
8356
8371
  } else {
8357
8372
  results.figma = { ok: null, message: "not configured" };
8358
8373
  }
8374
+ if (cfg.alation.baseUrl && cfg.alation.refreshToken && cfg.alation.userId !== void 0 && cfg.alation.userId !== "") {
8375
+ try {
8376
+ await validateAlationAccessToken(cfg);
8377
+ results.alation = { ok: true, message: "connected" };
8378
+ } catch (err) {
8379
+ results.alation = { ok: false, message: err instanceof Error ? err.message : String(err) };
8380
+ }
8381
+ } else {
8382
+ results.alation = { ok: null, message: "not configured" };
8383
+ }
8359
8384
  success(results, "config", "test", start);
8360
8385
  } catch (err) {
8361
8386
  fail(err, "config", "test", start);
@@ -8397,7 +8422,8 @@ function registerConfigCommands(program2) {
8397
8422
  ...dynamicEnvKeys,
8398
8423
  "logscale",
8399
8424
  "splitio",
8400
- "figma"
8425
+ "figma",
8426
+ "alation"
8401
8427
  ];
8402
8428
  if (cmdOpts.output === "table") {
8403
8429
  const labelWidth = Math.max(14, ...allServices.map((s) => s.length + 2));
@@ -9013,6 +9039,47 @@ async function initGlobalConfig(start) {
9013
9039
  }
9014
9040
  }
9015
9041
  }
9042
+ process.stderr.write("\n\u2500\u2500 Alation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
9043
+ const useAlation = await confirm({
9044
+ message: "Configure Alation for data catalog and Document Hub access?",
9045
+ default: false
9046
+ });
9047
+ let alationBaseUrl = "";
9048
+ let alationRefreshToken = "";
9049
+ let alationUserId = "";
9050
+ if (useAlation) {
9051
+ process.stderr.write(" Generate a refresh token in Alation under your profile \u2192 Settings \u2192 Authentication \u2192 Create Refresh Token.\n");
9052
+ process.stderr.write(" Note the numeric user ID shown with it; pncli exchanges the refresh token for short-lived API tokens itself.\n");
9053
+ alationBaseUrl = await input2({
9054
+ message: "Alation base URL (e.g. https://alation.imagile.dev):",
9055
+ validate: (v) => v.trim().length > 0 || "Required"
9056
+ });
9057
+ alationRefreshToken = await password({
9058
+ message: "Alation refresh token:"
9059
+ });
9060
+ alationUserId = await input2({
9061
+ message: "Alation user ID (numeric):",
9062
+ validate: (v) => /^\d+$/.test(v.trim()) || "Must be a positive integer"
9063
+ });
9064
+ if (alationBaseUrl && alationRefreshToken && alationUserId) {
9065
+ process.stderr.write("\n Verifying connection...\n");
9066
+ try {
9067
+ const tempConfig = {
9068
+ ...loadConfig(),
9069
+ alation: {
9070
+ baseUrl: normalizeBaseUrl(alationBaseUrl),
9071
+ refreshToken: alationRefreshToken,
9072
+ userId: alationUserId.trim()
9073
+ }
9074
+ };
9075
+ await validateAlationAccessToken(tempConfig);
9076
+ process.stderr.write(" Connected.\n");
9077
+ } catch (err) {
9078
+ warn(`Could not connect to Alation: ${err instanceof Error ? err.message : String(err)}`);
9079
+ warn("Config will be saved anyway. Check your URL, refresh token, and user ID and re-run pncli config init or pncli config test.");
9080
+ }
9081
+ }
9082
+ }
9016
9083
  process.stderr.write("\n\u2500\u2500 Defaults \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
9017
9084
  const jiraProject = await input2({
9018
9085
  message: "Default Jira project key (optional):",
@@ -9170,6 +9237,13 @@ async function initGlobalConfig(start) {
9170
9237
  token: figmaToken
9171
9238
  }
9172
9239
  } : {},
9240
+ ...useAlation && alationBaseUrl ? {
9241
+ alation: {
9242
+ baseUrl: normalizeBaseUrl(alationBaseUrl),
9243
+ refreshToken: alationRefreshToken || void 0,
9244
+ userId: alationUserId.trim() || void 0
9245
+ }
9246
+ } : {},
9173
9247
  defaults: {
9174
9248
  jira: {
9175
9249
  project: jiraProject || void 0
@@ -10058,10 +10132,10 @@ function registerAdoWorkCommands(ado) {
10058
10132
  const types = await discoverTypes(workClient, collection, project);
10059
10133
  if (opts.save) {
10060
10134
  const globalConfigPath = getGlobalConfigPath(globalOpts.config);
10061
- const fs7 = await import("fs");
10062
- const existing = JSON.parse(fs7.readFileSync(globalConfigPath, "utf8") || "{}");
10135
+ const fs8 = await import("fs");
10136
+ const existing = JSON.parse(fs8.readFileSync(globalConfigPath, "utf8") || "{}");
10063
10137
  existing.ado = { ...existing.ado ?? {}, discoveredTypes: types };
10064
- fs7.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
10138
+ fs8.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
10065
10139
  warn(`Saved ${types.length} types to ${globalConfigPath}`);
10066
10140
  }
10067
10141
  success(types, "ado", "work-types", start);
@@ -10090,11 +10164,11 @@ function registerAdoWorkCommands(ado) {
10090
10164
  ) : allFields;
10091
10165
  if (opts.save) {
10092
10166
  const globalConfigPath = getGlobalConfigPath(globalOpts.config);
10093
- const fs7 = await import("fs");
10094
- const existing = JSON.parse(fs7.readFileSync(globalConfigPath, "utf8") || "{}");
10167
+ const fs8 = await import("fs");
10168
+ const existing = JSON.parse(fs8.readFileSync(globalConfigPath, "utf8") || "{}");
10095
10169
  const aliases = buildDefaultAliases(allFields);
10096
10170
  existing.ado = { ...existing.ado ?? {}, discoveredFields: allFields, fieldAliases: aliases };
10097
- fs7.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
10171
+ fs8.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
10098
10172
  warn(`Saved ${allFields.length} fields and ${Object.keys(aliases).length} aliases to ${globalConfigPath}`);
10099
10173
  }
10100
10174
  success(fields, "ado", "work-fields", start);
@@ -10391,11 +10465,11 @@ function registerAdoRepoCommands(ado) {
10391
10465
  const start = Date.now();
10392
10466
  try {
10393
10467
  const globalOpts = ado.optsWithGlobals();
10394
- const { loadConfig: lc } = await import("./config-ZVQMQ6B2.js");
10468
+ const { loadConfig: lc } = await import("./config-Y6IE6GY7.js");
10395
10469
  const config = lc({ configPath: globalOpts.config });
10396
10470
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
10397
10471
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
10398
- const { createHttpClient: createHttpClient2 } = await import("./http-RX7E2V5C.js");
10472
+ const { createHttpClient: createHttpClient2 } = await import("./http-TAINU2LF.js");
10399
10473
  const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
10400
10474
  const coreClient = new AdoCoreClient2(http);
10401
10475
  const me = await coreClient.getConnectionData(collection);
@@ -12394,6 +12468,35 @@ Expected format: https://www.figma.com/design/<file-key>/...`);
12394
12468
  }
12395
12469
  return input3;
12396
12470
  }
12471
+ var NODE_ID_PATTERN = /^[A-Za-z0-9]+:[A-Za-z0-9]+(?:;[A-Za-z0-9]+:[A-Za-z0-9]+)*$/;
12472
+ var NODE_ID_HINT = "Expected a node ID like 12:34 or 12-34, or a Figma URL with a node-id query param";
12473
+ function isUrl(input3) {
12474
+ return input3.startsWith("http://") || input3.startsWith("https://");
12475
+ }
12476
+ function parseNodeId(input3) {
12477
+ let raw = input3;
12478
+ if (isUrl(input3)) {
12479
+ try {
12480
+ raw = new URL(input3).searchParams.get("node-id");
12481
+ } catch {
12482
+ raw = null;
12483
+ }
12484
+ if (raw === null) {
12485
+ throw new Error(`Could not extract a Figma node ID from URL: ${input3}
12486
+ ${NODE_ID_HINT}`);
12487
+ }
12488
+ }
12489
+ const nodeId = raw.trim().replace(/-/g, ":");
12490
+ if (!NODE_ID_PATTERN.test(nodeId)) {
12491
+ throw new Error(`Invalid Figma node ID: ${input3}
12492
+ ${NODE_ID_HINT}`);
12493
+ }
12494
+ return nodeId;
12495
+ }
12496
+ function findNodeIdInUrl(url) {
12497
+ if (!new URL(url).searchParams.has("node-id")) return void 0;
12498
+ return parseNodeId(url);
12499
+ }
12397
12500
  function getHttp5(program2) {
12398
12501
  const opts = program2.optsWithGlobals();
12399
12502
  return createHttpClient(
@@ -12403,11 +12506,38 @@ function getHttp5(program2) {
12403
12506
  }
12404
12507
  function registerFigmaCommands(program2) {
12405
12508
  const figma = program2.command("figma").description("Figma design operations (files, comments, versions)");
12406
- figma.command("file").description("Get a Figma file \u2014 metadata, structure, and component/style inventory").argument("<file-key-or-url>", "Figma file key or full Figma URL (https://www.figma.com/design/<key>/...)").option("--document", "Include the full document node tree (can be large; omitted by default)").action(async (fileKeyOrUrl, opts) => {
12509
+ figma.command("file").description("Get a Figma file \u2014 metadata, structure, and component/style inventory (or a single node with --node-id)").argument("<file-key-or-url>", "Figma file key or full Figma URL (https://www.figma.com/design/<key>/...); a node-id query param is auto-detected").option("--document", "Include the full document node tree (can be large; omitted by default)").option("--node-id <id>", "Fetch only this node and its descendants instead of the whole file (overrides any node-id detected in the URL)").action(async (fileKeyOrUrl, opts) => {
12407
12510
  const start = Date.now();
12408
12511
  const { success: success2, fail: fail2 } = await import("./output-HDUIIXUI.js");
12409
12512
  try {
12410
12513
  const fileKey = parseFileKey(fileKeyOrUrl);
12514
+ const nodeId = opts.nodeId ? parseNodeId(opts.nodeId) : isUrl(fileKeyOrUrl) ? findNodeIdInUrl(fileKeyOrUrl) : void 0;
12515
+ if (nodeId) {
12516
+ const data2 = await getHttp5(program2).figma(
12517
+ `/v1/files/${encodeURIComponent(fileKey)}/nodes`,
12518
+ { params: { ids: nodeId } }
12519
+ );
12520
+ const node = data2.nodes[nodeId];
12521
+ if (!node) {
12522
+ throw new Error(`Node ${nodeId} not found in file ${fileKey}`);
12523
+ }
12524
+ success2(
12525
+ {
12526
+ fileKey,
12527
+ nodeId,
12528
+ name: data2.name,
12529
+ lastModified: data2.lastModified,
12530
+ thumbnailUrl: data2.thumbnailUrl,
12531
+ document: node.document,
12532
+ componentCount: node.components ? Object.keys(node.components).length : 0,
12533
+ styleCount: node.styles ? Object.keys(node.styles).length : 0
12534
+ },
12535
+ "figma",
12536
+ "file",
12537
+ start
12538
+ );
12539
+ return;
12540
+ }
12411
12541
  const params = {};
12412
12542
  if (!opts.document) {
12413
12543
  params["depth"] = 1;
@@ -12496,6 +12626,410 @@ function registerFigmaCommands(program2) {
12496
12626
  });
12497
12627
  }
12498
12628
 
12629
+ // src/services/alation/commands.ts
12630
+ import fs7 from "fs";
12631
+ var MAX_PAGE = 1e3;
12632
+ function parseIntOption(value, flag) {
12633
+ if (value === void 0) return void 0;
12634
+ const trimmed = value.trim();
12635
+ if (!/^\d+$/.test(trimmed)) {
12636
+ throw new PncliError(`Invalid ${flag} "${value}". Expected a non-negative integer.`, 1);
12637
+ }
12638
+ return Number(trimmed);
12639
+ }
12640
+ function parsePositiveId(value, what) {
12641
+ const trimmed = value.trim();
12642
+ if (!/^[1-9]\d*$/.test(trimmed)) {
12643
+ throw new PncliError(`Invalid ${what} "${value}". Expected a positive integer ID.`, 1);
12644
+ }
12645
+ return Number(trimmed);
12646
+ }
12647
+ function pageParams(opts) {
12648
+ const limit = parseIntOption(opts.limit, "--limit") ?? 100;
12649
+ const skip = parseIntOption(opts.skip, "--skip") ?? 0;
12650
+ if (limit < 1 || limit > MAX_PAGE) {
12651
+ throw new PncliError(`--limit must be between 1 and ${MAX_PAGE} (Alation's per-request cap).`, 1);
12652
+ }
12653
+ return { limit, skip };
12654
+ }
12655
+ var SEARCH_OTYPES = [
12656
+ "api_resource",
12657
+ "api_resource_field",
12658
+ "api_resource_folder",
12659
+ "article",
12660
+ "bi_field",
12661
+ "catalog_set",
12662
+ "column",
12663
+ "dataflow",
12664
+ "datasource",
12665
+ "doc_schema",
12666
+ "docstore_collection",
12667
+ "docstore_folder",
12668
+ "domain",
12669
+ "execution_result",
12670
+ "file",
12671
+ "filesystem",
12672
+ "function",
12673
+ "glossary",
12674
+ "glossary_term",
12675
+ "glossary_v3",
12676
+ "group",
12677
+ "query_or_statement",
12678
+ "report_collection",
12679
+ "report_datasource",
12680
+ "report_object",
12681
+ "report_source",
12682
+ "schema",
12683
+ "table",
12684
+ "tag",
12685
+ "thread",
12686
+ "user",
12687
+ "value"
12688
+ ];
12689
+ function parseOtypes(raw) {
12690
+ if (raw === void 0) return void 0;
12691
+ const otypes = raw.split(",").map((s) => s.trim()).filter(Boolean);
12692
+ if (otypes.length === 0) throw new PncliError("--otype cannot be empty", 1);
12693
+ const bad = otypes.filter((o) => !SEARCH_OTYPES.includes(o));
12694
+ if (bad.length) {
12695
+ throw new PncliError(`Unknown --otype value(s): ${bad.join(", ")}. Valid: ${SEARCH_OTYPES.join(", ")}`, 1);
12696
+ }
12697
+ return otypes;
12698
+ }
12699
+ function toCustomFields(fields) {
12700
+ if (fields === void 0) return [];
12701
+ if (typeof fields !== "object" || fields === null || Array.isArray(fields)) {
12702
+ throw new PncliError('--input-file "fields" must be an object keyed by Alation custom field ID', 1);
12703
+ }
12704
+ return Object.entries(fields).map(([key, value]) => {
12705
+ if (!/^\d+$/.test(key)) {
12706
+ throw new PncliError(`--input-file "fields" key "${key}" is not a numeric Alation field_id`, 1);
12707
+ }
12708
+ return { field_id: Number(key), value: resolveAtFileRef(value) };
12709
+ });
12710
+ }
12711
+ var ALATION_DOCUMENT_INPUT_FILE_SCHEMA = {
12712
+ type: "object",
12713
+ properties: {
12714
+ title: { type: "string", description: "Document title" },
12715
+ description: { type: "string", description: 'Document summary; "@path" reads the value from a file' },
12716
+ hub: { type: "integer", description: "document_hub_id (required on create)" },
12717
+ folder: { type: "integer", description: "parent_folder_id (required on create)" },
12718
+ template: { type: "integer", description: "template_id; required whenever fields are supplied" },
12719
+ parentDocument: { type: "integer", description: "parent_document_id for a sub-document" },
12720
+ navLinkFolders: { type: "array", items: { type: "integer" }, description: "nav_link_folder_ids" },
12721
+ fields: {
12722
+ type: "object",
12723
+ description: 'Custom field values keyed by numeric Alation field_id; string values starting with "@" are read from that file',
12724
+ additionalProperties: true
12725
+ }
12726
+ }
12727
+ };
12728
+ var ALATION_DOCUMENT_INPUT_FILE_EXAMPLE = {
12729
+ title: "Customer table onboarding notes",
12730
+ description: "@docs/customer-notes.md",
12731
+ hub: 1,
12732
+ folder: 42,
12733
+ template: 7,
12734
+ fields: { "10001": "Team Data Platform" }
12735
+ };
12736
+ function getHttp6(program2) {
12737
+ const opts = program2.optsWithGlobals();
12738
+ return createHttpClient(
12739
+ loadConfig({ configPath: opts.config }),
12740
+ Boolean(opts.dryRun)
12741
+ );
12742
+ }
12743
+ async function getOne(http, path10, id, what) {
12744
+ const rows = await http.alation(path10, { params: { id, limit: 1 } });
12745
+ const row = Array.isArray(rows) ? rows.find((r) => r.id === id) ?? rows[0] : void 0;
12746
+ if (!row) throw new PncliError(`${what} ${id} not found`, 404);
12747
+ return row;
12748
+ }
12749
+ function buildDocumentPayload(opts) {
12750
+ const fromJson = {};
12751
+ if (opts.inputFile) {
12752
+ const parsed = readJsonInputFile(opts.inputFile);
12753
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
12754
+ throw new PncliError("--input-file must contain a JSON object. Run: pncli alation document schema", 1);
12755
+ }
12756
+ Object.assign(fromJson, parsed);
12757
+ }
12758
+ const fromFlags = {
12759
+ title: opts.title,
12760
+ description: opts.description,
12761
+ hub: opts.hub !== void 0 ? parsePositiveId(opts.hub, "--hub") : void 0,
12762
+ folder: opts.folder !== void 0 ? parsePositiveId(opts.folder, "--folder") : void 0,
12763
+ template: opts.template !== void 0 ? parsePositiveId(opts.template, "--template") : void 0,
12764
+ parentDocument: opts.parentDocument !== void 0 ? parsePositiveId(opts.parentDocument, "--parent-document") : void 0
12765
+ };
12766
+ const { merged, overrides } = mergeWithOverrides(fromJson, fromFlags);
12767
+ if (overrides.length) warn(`--input-file values overridden by flags: ${overrides.join(", ")}`);
12768
+ const payload = {};
12769
+ if (merged.title !== void 0) payload.title = merged.title;
12770
+ if (merged.description !== void 0) payload.description = resolveAtFileRef(merged.description);
12771
+ if (merged.hub !== void 0) payload.document_hub_id = merged.hub;
12772
+ if (merged.folder !== void 0) payload.parent_folder_id = merged.folder;
12773
+ if (merged.template !== void 0) payload.template_id = merged.template;
12774
+ if (merged.parentDocument !== void 0) payload.parent_document_id = merged.parentDocument;
12775
+ if (merged.navLinkFolders !== void 0) payload.nav_link_folder_ids = merged.navLinkFolders;
12776
+ const customFields = toCustomFields(merged.fields);
12777
+ if (customFields.length) {
12778
+ if (payload.template_id === void 0) {
12779
+ throw new PncliError('Alation requires template_id whenever custom fields are set. Pass --template <id> or "template" in --input-file.', 1);
12780
+ }
12781
+ payload.custom_fields = customFields;
12782
+ }
12783
+ return { payload, overrides };
12784
+ }
12785
+ function registerAlationCommands(program2) {
12786
+ const alation = program2.command("alation").description("Alation data catalog (data sources, schemas, tables, columns, search, documents)");
12787
+ const token = alation.command("token").description("API access token operations");
12788
+ token.command("status").description("Mint an API access token from the configured refresh token and report its expiry (verifies credentials)").action(async () => {
12789
+ const start = Date.now();
12790
+ try {
12791
+ const opts = program2.optsWithGlobals();
12792
+ const config = loadConfig({ configPath: opts.config });
12793
+ if (opts.dryRun) {
12794
+ fs7.writeSync(process.stderr.fd, `DRY RUN: POST ${config.alation.baseUrl ?? "<alation.baseUrl>"}/integration/v1/createAPIAccessToken/ (token exchange skipped)
12795
+ `);
12796
+ process.exitCode = ExitCode.SUCCESS;
12797
+ throw new PncliError("dry-run", 0);
12798
+ }
12799
+ const data = await validateAlationAccessToken(config);
12800
+ success(data, "alation", "token-status", start);
12801
+ } catch (err) {
12802
+ fail(err, "alation", "token-status", start);
12803
+ }
12804
+ });
12805
+ const datasource = alation.command("datasource").description("Data source operations");
12806
+ datasource.command("list").description("List data sources").option("--include-undeployed", "Include data sources whose setup is incomplete").option("--include-hidden", "Include hidden data sources").action(async (opts) => {
12807
+ const start = Date.now();
12808
+ try {
12809
+ const params = {};
12810
+ if (opts.includeUndeployed) params.include_undeployed = true;
12811
+ if (opts.includeHidden) params.include_hidden = true;
12812
+ const data = await getHttp6(program2).alation("integration/v1/datasource/", { params });
12813
+ success({ count: data.length, datasources: data }, "alation", "datasource-list", start);
12814
+ } catch (err) {
12815
+ fail(err, "alation", "datasource-list", start);
12816
+ }
12817
+ });
12818
+ datasource.command("get").description("Get a data source by ID").argument("<id>", "Data source ID").action(async (id) => {
12819
+ const start = Date.now();
12820
+ try {
12821
+ const dsId = parsePositiveId(id, "data source ID");
12822
+ const data = await getHttp6(program2).alation(`integration/v1/datasource/${dsId}/`);
12823
+ success(data, "alation", "datasource-get", start);
12824
+ } catch (err) {
12825
+ fail(err, "alation", "datasource-get", start);
12826
+ }
12827
+ });
12828
+ const schema = alation.command("schema").description("Schema operations");
12829
+ schema.command("list").description("List schemas, optionally within one data source").option("--ds <id>", "Filter by data source ID").option("--name <name>", "Filter by exact schema name").option("--limit <n>", "Page size (max 1000)", "100").option("--skip <n>", "Number of results to skip", "0").action(async (opts) => {
12830
+ const start = Date.now();
12831
+ try {
12832
+ const params = { ...pageParams(opts) };
12833
+ if (opts.ds) params.ds_id = parsePositiveId(opts.ds, "--ds");
12834
+ if (opts.name) params.name = opts.name;
12835
+ const data = await getHttp6(program2).alation("integration/v2/schema/", { params });
12836
+ success({ count: data.length, limit: params.limit, skip: params.skip, schemas: data }, "alation", "schema-list", start);
12837
+ } catch (err) {
12838
+ fail(err, "alation", "schema-list", start);
12839
+ }
12840
+ });
12841
+ schema.command("get").description("Get a schema by ID").argument("<id>", "Schema ID").action(async (id) => {
12842
+ const start = Date.now();
12843
+ try {
12844
+ const data = await getOne(getHttp6(program2), "integration/v2/schema/", parsePositiveId(id, "schema ID"), "Schema");
12845
+ success(data, "alation", "schema-get", start);
12846
+ } catch (err) {
12847
+ fail(err, "alation", "schema-get", start);
12848
+ }
12849
+ });
12850
+ const table = alation.command("table").description("Table operations");
12851
+ table.command("list").description("List tables (descriptions, custom fields, fully-qualified keys)").option("--ds <id>", "Filter by data source ID").option("--schema <id>", "Filter by schema ID").option("--schema-name <name>", "Filter by schema name").option("--name <name>", "Filter by exact table name").option("--search <text>", "Case-insensitive substring match on table name").option("--limit <n>", "Page size (max 1000)", "100").option("--skip <n>", "Number of results to skip", "0").action(async (opts) => {
12852
+ const start = Date.now();
12853
+ try {
12854
+ const params = { ...pageParams(opts) };
12855
+ if (opts.ds) params.ds_id = parsePositiveId(opts.ds, "--ds");
12856
+ if (opts.schema) params.schema_id = parsePositiveId(opts.schema, "--schema");
12857
+ if (opts.schemaName) params.schema_name = opts.schemaName;
12858
+ if (opts.name) params.name = opts.name;
12859
+ if (opts.search) params.name__icontains = opts.search;
12860
+ const data = await getHttp6(program2).alation("integration/v2/table/", { params });
12861
+ success({ count: data.length, limit: params.limit, skip: params.skip, tables: data }, "alation", "table-list", start);
12862
+ } catch (err) {
12863
+ fail(err, "alation", "table-list", start);
12864
+ }
12865
+ });
12866
+ table.command("get").description("Get a table by ID, optionally with its columns").argument("<id>", "Table ID").option("--columns", "Also fetch the table's columns (up to 1000)").action(async (id, opts) => {
12867
+ const start = Date.now();
12868
+ try {
12869
+ const http = getHttp6(program2);
12870
+ const tableId = parsePositiveId(id, "table ID");
12871
+ const data = await getOne(http, "integration/v2/table/", tableId, "Table");
12872
+ if (opts.columns) {
12873
+ const columns = await http.alation("integration/v2/column/", {
12874
+ params: { table_id: tableId, limit: MAX_PAGE, order_by: "id" }
12875
+ });
12876
+ success({ ...data, columnCount: columns.length, columns }, "alation", "table-get", start);
12877
+ return;
12878
+ }
12879
+ success(data, "alation", "table-get", start);
12880
+ } catch (err) {
12881
+ fail(err, "alation", "table-get", start);
12882
+ }
12883
+ });
12884
+ const column = alation.command("column").description("Column operations");
12885
+ column.command("list").description("List columns of a table (types, descriptions, keys, custom fields)").option("--table <id>", "Filter by table ID").option("--table-name <name>", "Filter by table name").option("--schema <id>", "Filter by schema ID").option("--ds <id>", "Filter by data source ID").option("--name <name>", "Filter by exact column name").option("--search <text>", "Case-insensitive substring match on column name").option("--limit <n>", "Page size (max 1000)", "100").option("--skip <n>", "Number of results to skip", "0").action(async (opts) => {
12886
+ const start = Date.now();
12887
+ try {
12888
+ if (!opts.table && !opts.tableName && !opts.schema && !opts.ds && !opts.name && !opts.search) {
12889
+ throw new PncliError("Pass at least one filter (--table, --table-name, --schema, --ds, --name, or --search); listing every column in the catalog is not supported.", 1);
12890
+ }
12891
+ const params = { ...pageParams(opts) };
12892
+ if (opts.table) params.table_id = parsePositiveId(opts.table, "--table");
12893
+ if (opts.tableName) params.table_name = opts.tableName;
12894
+ if (opts.schema) params.schema_id = parsePositiveId(opts.schema, "--schema");
12895
+ if (opts.ds) params.ds_id = parsePositiveId(opts.ds, "--ds");
12896
+ if (opts.name) params.name = opts.name;
12897
+ if (opts.search) params.name__icontains = opts.search;
12898
+ const data = await getHttp6(program2).alation("integration/v2/column/", { params });
12899
+ success({ count: data.length, limit: params.limit, skip: params.skip, columns: data }, "alation", "column-list", start);
12900
+ } catch (err) {
12901
+ fail(err, "alation", "column-list", start);
12902
+ }
12903
+ });
12904
+ column.command("get").description("Get a column by ID").argument("<id>", "Column ID").action(async (id) => {
12905
+ const start = Date.now();
12906
+ try {
12907
+ const data = await getOne(getHttp6(program2), "integration/v2/column/", parsePositiveId(id, "column ID"), "Column");
12908
+ success(data, "alation", "column-get", start);
12909
+ } catch (err) {
12910
+ fail(err, "alation", "column-get", start);
12911
+ }
12912
+ });
12913
+ alation.command("search").description("Full-text search across the catalog").argument("<query>", "Search text").option("--otype <types>", `Comma-separated object types to restrict to (e.g. table,column). Valid: ${SEARCH_OTYPES.join(", ")}`).option("--limit <n>", "Maximum results (default 20, max 10000)", "20").option("--offset <n>", "Index of the first result", "0").action(async (query, opts) => {
12914
+ const start = Date.now();
12915
+ try {
12916
+ const limit = parseIntOption(opts.limit, "--limit") ?? 20;
12917
+ const offset = parseIntOption(opts.offset, "--offset") ?? 0;
12918
+ if (limit > 1e4) throw new PncliError("--limit must be at most 10000", 1);
12919
+ const params = { q: query, limit, offset };
12920
+ const otypes = parseOtypes(opts.otype);
12921
+ if (otypes) params.filters = JSON.stringify({ otypes });
12922
+ const data = await getHttp6(program2).alation("integration/v1/search/", { params });
12923
+ success(
12924
+ { query, total: data.total, limit: data.limit, offset: data.offset, fullSearchUrl: data.full_search_url, results: data.results },
12925
+ "alation",
12926
+ "search",
12927
+ start
12928
+ );
12929
+ } catch (err) {
12930
+ fail(err, "alation", "search", start);
12931
+ }
12932
+ });
12933
+ const folder = alation.command("folder").description("Document Hub folder operations");
12934
+ folder.command("list").description("List folders in a Document Hub").option("--hub <id>", "Document hub ID").option("--parent <id>", "Parent folder ID (list its subfolders)").option("--search <text>", "Filter on textual fields").option("--limit <n>", "Page size (max 1000)", "100").option("--skip <n>", "Number of results to skip", "0").action(async (opts) => {
12935
+ const start = Date.now();
12936
+ try {
12937
+ const params = { ...pageParams(opts) };
12938
+ if (opts.hub) params.document_hub_id = parsePositiveId(opts.hub, "--hub");
12939
+ if (opts.parent) params.parent_folder_id = parsePositiveId(opts.parent, "--parent");
12940
+ if (opts.search) params.search = opts.search;
12941
+ const data = await getHttp6(program2).alation("integration/v2/folder/", { params });
12942
+ success({ count: data.length, limit: params.limit, skip: params.skip, folders: data }, "alation", "folder-list", start);
12943
+ } catch (err) {
12944
+ fail(err, "alation", "folder-list", start);
12945
+ }
12946
+ });
12947
+ folder.command("get").description("Get a Document Hub folder by ID").argument("<id>", "Folder ID").action(async (id) => {
12948
+ const start = Date.now();
12949
+ try {
12950
+ const data = await getOne(getHttp6(program2), "integration/v2/folder/", parsePositiveId(id, "folder ID"), "Folder");
12951
+ success(data, "alation", "folder-get", start);
12952
+ } catch (err) {
12953
+ fail(err, "alation", "folder-get", start);
12954
+ }
12955
+ });
12956
+ const document = alation.command("document").description("Document Hub document operations");
12957
+ document.command("list").description("List documents in a Document Hub or folder").option("--hub <id>", "Document hub ID").option("--folder <id>", "Parent folder ID").option("--parent-document <id>", "Parent document ID (list sub-documents)").option("--search <text>", "Filter on all textual fields").option("--deleted", "Return only deleted documents").option("--limit <n>", "Page size (max 1000)", "100").option("--skip <n>", "Number of results to skip", "0").action(async (opts) => {
12958
+ const start = Date.now();
12959
+ try {
12960
+ const params = { ...pageParams(opts) };
12961
+ if (opts.hub) params.document_hub_id = parsePositiveId(opts.hub, "--hub");
12962
+ if (opts.folder) params.parent_folder_id = parsePositiveId(opts.folder, "--folder");
12963
+ if (opts.parentDocument) params.parent_document_id = parsePositiveId(opts.parentDocument, "--parent-document");
12964
+ if (opts.search) params.search = opts.search;
12965
+ if (opts.deleted) params.deleted = true;
12966
+ const data = await getHttp6(program2).alation("integration/v2/document/", { params });
12967
+ success({ count: data.length, limit: params.limit, skip: params.skip, documents: data }, "alation", "document-list", start);
12968
+ } catch (err) {
12969
+ fail(err, "alation", "document-list", start);
12970
+ }
12971
+ });
12972
+ document.command("get").description("Get a document by ID, including its custom field values").argument("<id>", "Document ID").action(async (id) => {
12973
+ const start = Date.now();
12974
+ try {
12975
+ const data = await getOne(getHttp6(program2), "integration/v2/document/", parsePositiveId(id, "document ID"), "Document");
12976
+ success(data, "alation", "document-get", start);
12977
+ } catch (err) {
12978
+ fail(err, "alation", "document-get", start);
12979
+ }
12980
+ });
12981
+ document.command("schema").description("Print the --input-file JSON schema and an example for document create/update").option("--example-only", "Print only the runnable example JSON (no envelope) \u2014 pipeable straight into --input-file").action((opts) => {
12982
+ const start = Date.now();
12983
+ if (opts.exampleOnly) {
12984
+ writeRawOutput(JSON.stringify(ALATION_DOCUMENT_INPUT_FILE_EXAMPLE, null, 2) + "\n");
12985
+ return;
12986
+ }
12987
+ success({ schema: ALATION_DOCUMENT_INPUT_FILE_SCHEMA, example: ALATION_DOCUMENT_INPUT_FILE_EXAMPLE }, "alation", "document-schema", start);
12988
+ });
12989
+ document.command("create").description("Create a document (async \u2014 returns a job ID; check it with `alation job get`)").option("--title <title>", "Document title").option("--description <text>", "Document summary").option("--hub <id>", "Document hub ID").option("--folder <id>", "Parent folder ID").option("--template <id>", "Template ID (required when custom fields are set)").option("--parent-document <id>", "Parent document ID for a sub-document").option("--input-file <path>", 'JSON file with title/description/hub/folder/template/fields (see `alation document schema`); "-" reads stdin').action(async (opts) => {
12990
+ const start = Date.now();
12991
+ try {
12992
+ const { payload, overrides } = buildDocumentPayload(opts);
12993
+ for (const [key, flag] of [["title", "--title"], ["document_hub_id", "--hub"], ["parent_folder_id", "--folder"]]) {
12994
+ if (payload[key] === void 0) throw new PncliError(`${flag} is required to create a document (or set it in --input-file)`, 1);
12995
+ }
12996
+ const data = await getHttp6(program2).alation("integration/v2/document/", { method: "POST", body: [payload] });
12997
+ success({ jobId: data.job_id, document: payload, ...overrides.length ? { overrides } : {} }, "alation", "document-create", start);
12998
+ } catch (err) {
12999
+ fail(err, "alation", "document-create", start);
13000
+ }
13001
+ });
13002
+ document.command("update").description("Update a document (async \u2014 returns a job ID; custom field values replace existing values per field)").argument("<id>", "Document ID").option("--title <title>", "New title").option("--description <text>", "New summary").option("--folder <id>", "Move to this parent folder").option("--template <id>", "Template ID (required when custom fields are set)").option("--input-file <path>", 'JSON file with title/description/folder/template/fields (see `alation document schema`); "-" reads stdin').action(async (id, opts) => {
13003
+ const start = Date.now();
13004
+ try {
13005
+ const documentId = parsePositiveId(id, "document ID");
13006
+ const { payload, overrides } = buildDocumentPayload(opts);
13007
+ if (payload.document_hub_id !== void 0) {
13008
+ throw new PncliError('A document cannot change hubs; remove "hub" from --input-file.', 1);
13009
+ }
13010
+ if (Object.keys(payload).length === 0) {
13011
+ throw new PncliError("Nothing to update. Pass --title, --description, --folder, or --input-file.", 1);
13012
+ }
13013
+ const body = [{ id: documentId, ...payload }];
13014
+ const data = await getHttp6(program2).alation("integration/v2/document/", { method: "PUT", body });
13015
+ success({ jobId: data.job_id, document: body[0], ...overrides.length ? { overrides } : {} }, "alation", "document-update", start);
13016
+ } catch (err) {
13017
+ fail(err, "alation", "document-update", start);
13018
+ }
13019
+ });
13020
+ const job = alation.command("job").description("Background job operations");
13021
+ job.command("get").description("Get the status of a bulk job (returned by document create/update)").argument("<id>", "Job ID").action(async (id) => {
13022
+ const start = Date.now();
13023
+ try {
13024
+ const jobId = parsePositiveId(id, "job ID");
13025
+ const data = await getHttp6(program2).alation("api/v1/bulk_metadata/job/", { params: { id: jobId } });
13026
+ success({ jobId, ...data }, "alation", "job-get", start);
13027
+ } catch (err) {
13028
+ fail(err, "alation", "job-get", start);
13029
+ }
13030
+ });
13031
+ }
13032
+
12499
13033
  // src/cli.ts
12500
13034
  if (!process.env.PNCLI_VERIFY_TLS) {
12501
13035
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
@@ -12541,6 +13075,7 @@ registerDynatraceCommands(program);
12541
13075
  registerLogscaleCommands(program);
12542
13076
  registerSplitioCommands(program);
12543
13077
  registerFigmaCommands(program);
13078
+ registerAlationCommands(program);
12544
13079
  program.addHelpText("after", `
12545
13080
  Services:
12546
13081
  git Local git operations (status, diff, log, branch)
@@ -12562,6 +13097,7 @@ Services:
12562
13097
  logscale LogScale (log query, repositories)
12563
13098
  splitio Split.IO (feature flags, Change Requests)
12564
13099
  figma Figma (files, comments, version history)
13100
+ alation Alation (data sources, schemas, tables, columns, search, Document Hubs)
12565
13101
  config Manage pncli configuration
12566
13102
  doctor Diagnose pncli setup (config files, credentials, agent skills)
12567
13103
  skills Install and manage agent skills (Codex, GitHub Copilot, Claude Code)