deepline 0.2.70 → 0.2.72

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/index.js CHANGED
@@ -1044,7 +1044,7 @@ var SDK_RELEASE = {
1044
1044
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
1045
1045
  // exposed storage-dependent synchronous access. This deliberate minor
1046
1046
  // release keeps lazy paging semantics independent of row residency.
1047
- version: "0.2.70",
1047
+ version: "0.2.72",
1048
1048
  contracts: {
1049
1049
  api: {
1050
1050
  name: "sdk-http-api",
@@ -3983,16 +3983,9 @@ function isPrebuiltPlayDescription(play) {
3983
3983
  return play.origin === "prebuilt" || play.ownerType === "deepline";
3984
3984
  }
3985
3985
  function preferPrebuiltPlayDescriptions(plays) {
3986
- const prebuilt = [];
3987
- const owned = [];
3988
- for (const play of plays) {
3989
- if (isPrebuiltPlayDescription(play)) {
3990
- prebuilt.push(play);
3991
- } else {
3992
- owned.push(play);
3993
- }
3994
- }
3995
- return [...prebuilt, ...owned];
3986
+ return plays.map((play, index) => ({ play, index })).sort(
3987
+ (left, right) => Number(right.play.pinned) - Number(left.play.pinned) || Number(isPrebuiltPlayDescription(right.play)) - Number(isPrebuiltPlayDescription(left.play)) || left.index - right.index
3988
+ ).map(({ play }) => play);
3996
3989
  }
3997
3990
  function isPlayRunPackage(value) {
3998
3991
  return Boolean(
@@ -4339,6 +4332,8 @@ var DeeplineClient = class {
4339
4332
  ...play.reference ? { reference: play.reference } : {},
4340
4333
  ...play.displayName ? { displayName: play.displayName } : {},
4341
4334
  ...description ? { description } : {},
4335
+ pinned: Boolean(play.pinned),
4336
+ toolCategories: play.toolCategories ?? [],
4342
4337
  origin: play.origin,
4343
4338
  ownerType: play.ownerType,
4344
4339
  canEdit: play.canEdit,
@@ -5837,6 +5832,16 @@ var DeeplineClient = class {
5837
5832
  async listPlays(options) {
5838
5833
  const params = new URLSearchParams();
5839
5834
  if (options?.origin) params.set("origin", options.origin);
5835
+ if (options?.categories) {
5836
+ params.set(
5837
+ "categories",
5838
+ Array.isArray(options.categories) ? options.categories.join(",") : options.categories
5839
+ );
5840
+ }
5841
+ if (options?.categories || options?.includeToolCategories) {
5842
+ params.set("include_tool_categories", "1");
5843
+ }
5844
+ if (options?.includeArchived) params.set("include_archived", "1");
5840
5845
  if (options?.grep?.trim()) {
5841
5846
  params.set("grep", options.grep.trim());
5842
5847
  params.set("grep_mode", options.grepMode ?? "all");
@@ -5848,6 +5853,12 @@ var DeeplineClient = class {
5848
5853
  );
5849
5854
  return response.plays ?? [];
5850
5855
  }
5856
+ /** Set whether an org-owned Play sorts before unpinned Plays. */
5857
+ async setPlayPinned(playName, pinned) {
5858
+ return this.http.post(`/api/v2/plays/${encodeURIComponent(playName)}/pin`, {
5859
+ pinned
5860
+ });
5861
+ }
5851
5862
  /** Read product-notification destinations, subscriptions, event catalog, and DLQ health. */
5852
5863
  async getNotificationSettings() {
5853
5864
  return this.http.get("/api/v2/settings/notifications");
@@ -5981,9 +5992,10 @@ var DeeplineClient = class {
5981
5992
  * console.log(`Total runs: ${detail.play.runCount}`);
5982
5993
  * ```
5983
5994
  */
5984
- async getPlay(name) {
5995
+ async getPlay(name, options) {
5985
5996
  const encodedName = encodeURIComponent(name);
5986
- return this.http.get(`/api/v2/plays/${encodedName}`);
5997
+ const query = options?.source ? `?include=source&revision=${encodeURIComponent(options.source)}` : "";
5998
+ return this.http.get(`/api/v2/plays/${encodedName}${query}`);
5987
5999
  }
5988
6000
  /**
5989
6001
  * Get a normalized play description suitable for agents and CLIs.
@@ -18580,7 +18592,10 @@ async function assertCanonicalNamedPlayReference(client2, target, options = {})
18580
18592
  const parsed = parseReferencedPlayTarget2(target);
18581
18593
  let detail;
18582
18594
  try {
18583
- detail = await client2.getPlay(parsed.playName);
18595
+ detail = await client2.getPlay(
18596
+ parsed.playName,
18597
+ options.source ? { source: options.source } : void 0
18598
+ );
18584
18599
  } catch (error) {
18585
18600
  if (isPlayNotFoundError(error)) {
18586
18601
  throw await buildPlayReferenceNotFoundError({
@@ -18633,19 +18648,54 @@ function materializeRemotePlaySource(input2) {
18633
18648
  if (isFileTarget(input2.target)) {
18634
18649
  return null;
18635
18650
  }
18636
- if (!input2.sourceCode.trim()) {
18651
+ const entrySource = input2.source.files[input2.source.entryFile];
18652
+ if (!entrySource?.trim()) {
18637
18653
  return null;
18638
18654
  }
18655
+ const files = Object.entries(input2.source.files);
18656
+ if (files.length > 1) {
18657
+ if (!input2.outPath) {
18658
+ throw new Error(
18659
+ `Play ${input2.playName} has ${files.length} source files. Use --out <directory> to write the complete bundle.`
18660
+ );
18661
+ }
18662
+ const outputDirectory = (0, import_node_path14.resolve)(input2.outPath);
18663
+ let created = 0;
18664
+ let updated = 0;
18665
+ for (const [logicalPath, sourceCode] of files) {
18666
+ const outputPath2 = (0, import_node_path14.resolve)(outputDirectory, logicalPath);
18667
+ const outputRelativePath = (0, import_node_path14.relative)(outputDirectory, outputPath2);
18668
+ if (!outputRelativePath || outputRelativePath.startsWith("..") || (0, import_node_path14.isAbsolute)(outputRelativePath)) {
18669
+ throw new Error(
18670
+ `Refusing to materialize unsafe Play source path ${JSON.stringify(logicalPath)}.`
18671
+ );
18672
+ }
18673
+ (0, import_node_fs12.mkdirSync)((0, import_node_path14.dirname)(outputPath2), { recursive: true });
18674
+ if (!(0, import_node_fs12.existsSync)(outputPath2)) {
18675
+ (0, import_node_fs12.writeFileSync)(outputPath2, sourceCode, "utf-8");
18676
+ created += 1;
18677
+ } else if ((0, import_node_fs12.readFileSync)(outputPath2, "utf-8") !== sourceCode) {
18678
+ (0, import_node_fs12.writeFileSync)(outputPath2, sourceCode, "utf-8");
18679
+ updated += 1;
18680
+ }
18681
+ }
18682
+ return {
18683
+ path: outputDirectory,
18684
+ status: created > 0 ? "created" : updated > 0 ? "updated" : "unchanged",
18685
+ created: created > 0,
18686
+ fileCount: files.length
18687
+ };
18688
+ }
18639
18689
  const outputPath = input2.outPath ?? defaultMaterializedPlayPath(input2.playName);
18640
18690
  if ((0, import_node_fs12.existsSync)(outputPath)) {
18641
18691
  const existingSource = (0, import_node_fs12.readFileSync)(outputPath, "utf-8");
18642
- if (existingSource === input2.sourceCode) {
18692
+ if (existingSource === entrySource) {
18643
18693
  return { path: outputPath, status: "unchanged", created: false };
18644
18694
  }
18645
- (0, import_node_fs12.writeFileSync)(outputPath, input2.sourceCode, "utf-8");
18695
+ (0, import_node_fs12.writeFileSync)(outputPath, entrySource, "utf-8");
18646
18696
  return { path: outputPath, status: "updated", created: false };
18647
18697
  }
18648
- (0, import_node_fs12.writeFileSync)(outputPath, input2.sourceCode, "utf-8");
18698
+ (0, import_node_fs12.writeFileSync)(outputPath, entrySource, "utf-8");
18649
18699
  return { path: outputPath, status: "created", created: true };
18650
18700
  }
18651
18701
  function formatLoadedPlayMessage(materializedFile) {
@@ -22518,6 +22568,47 @@ ${PLAY_PUBLISH_USAGE}`
22518
22568
  jsonOutput: argsWantJson(args)
22519
22569
  };
22520
22570
  }
22571
+ var PLAY_SAVE_USAGE = "Usage: deepline plays save <play-file.ts> [--expected-artifact <hash>] [--dry-run] [--json]";
22572
+ function parsePlaySaveOptions(args) {
22573
+ const target = args[0];
22574
+ if (!target) throw new Error(PLAY_SAVE_USAGE);
22575
+ if (target.startsWith("-")) {
22576
+ throw new Error(`${PLAY_SAVE_USAGE}
22577
+ The Play file is positional.`);
22578
+ }
22579
+ let expectedArtifactHash;
22580
+ let dryRun = false;
22581
+ for (let index = 1; index < args.length; index += 1) {
22582
+ const arg = args[index];
22583
+ if (arg === "--expected-artifact") {
22584
+ expectedArtifactHash = args[++index];
22585
+ if (!expectedArtifactHash) {
22586
+ throw new Error("--expected-artifact requires an artifact hash.");
22587
+ }
22588
+ continue;
22589
+ }
22590
+ if (arg.startsWith("--expected-artifact=")) {
22591
+ expectedArtifactHash = arg.slice("--expected-artifact=".length);
22592
+ if (!expectedArtifactHash) {
22593
+ throw new Error("--expected-artifact requires an artifact hash.");
22594
+ }
22595
+ continue;
22596
+ }
22597
+ if (arg === "--dry-run") {
22598
+ dryRun = true;
22599
+ continue;
22600
+ }
22601
+ if (arg === "--json") continue;
22602
+ throw new Error(`Unknown option: ${arg}
22603
+ ${PLAY_SAVE_USAGE}`);
22604
+ }
22605
+ return {
22606
+ target,
22607
+ expectedArtifactHash,
22608
+ dryRun,
22609
+ jsonOutput: argsWantJson(args)
22610
+ };
22611
+ }
22521
22612
  function formatByteBudget(usedBytes, limitBytes) {
22522
22613
  return `${usedBytes.toLocaleString()} B / ${limitBytes.toLocaleString()} B`;
22523
22614
  }
@@ -22848,6 +22939,29 @@ function resolvePlayCheckExportNames(sourceCode) {
22848
22939
  if (!exports2 || exports2.length === 0) return [PLAY_DEFAULT_EXPORT];
22849
22940
  return exports2.map((entry) => entry.name);
22850
22941
  }
22942
+ function artifactForProductionCompatibilityCheck(artifact) {
22943
+ const rawEdition = process.env.DEEPLINE_PROD_CHECK_AUTHORING_CONTRACT_EDITION?.trim();
22944
+ if (!rawEdition) return artifact;
22945
+ if (!/^\d+$/.test(rawEdition)) {
22946
+ throw new Error(
22947
+ "DEEPLINE_PROD_CHECK_AUTHORING_CONTRACT_EDITION must be a positive integer."
22948
+ );
22949
+ }
22950
+ const edition = Number(rawEdition);
22951
+ if (edition < 1) {
22952
+ throw new Error(
22953
+ "DEEPLINE_PROD_CHECK_AUTHORING_CONTRACT_EDITION must be a positive integer."
22954
+ );
22955
+ }
22956
+ const compatibility = isRecord10(artifact.compatibility) ? artifact.compatibility : {};
22957
+ return {
22958
+ ...artifact,
22959
+ compatibility: {
22960
+ ...compatibility,
22961
+ authoringContractEdition: edition
22962
+ }
22963
+ };
22964
+ }
22851
22965
  async function checkOneExportedPlay(input2) {
22852
22966
  const { absolutePlayPath, sourceCode, exportName } = input2;
22853
22967
  const fallbackName = extractPlayName(sourceCode, absolutePlayPath);
@@ -22887,7 +23001,7 @@ async function checkOneExportedPlay(input2) {
22887
23001
  sourceCode: graph.root.sourceCode,
22888
23002
  sourceFiles: graph.root.sourceFiles,
22889
23003
  description: graph.root.playDescription ?? void 0,
22890
- artifact: graph.root.artifact,
23004
+ artifact: artifactForProductionCompatibilityCheck(graph.root.artifact),
22891
23005
  ...exportName === PLAY_DEFAULT_EXPORT ? {} : { exportName },
22892
23006
  ...importedPlays.length > 0 ? { importedPlays } : {},
22893
23007
  ...integrationMode ? { integrationMode } : {}
@@ -23958,24 +24072,63 @@ async function handlePlayGet(args) {
23958
24072
  const explicitJson = args.includes("--json");
23959
24073
  const sourceOutput = args.includes("--source");
23960
24074
  const jsonOutput = sourceOutput ? explicitJson : argsWantJson(args);
24075
+ const requestedRevisionFlags = [
24076
+ args.includes("--working"),
24077
+ args.includes("--live"),
24078
+ args.includes("--revision")
24079
+ ].filter(Boolean).length;
24080
+ if (requestedRevisionFlags > 1) {
24081
+ console.error("Use only one of --working, --live, or --revision <number>.");
24082
+ return 2;
24083
+ }
24084
+ let sourceSelector = "working";
24085
+ if (args.includes("--live")) {
24086
+ sourceSelector = "live";
24087
+ }
23961
24088
  let outPath = null;
24089
+ let requestedVersion = null;
23962
24090
  for (let index = 1; index < args.length; index += 1) {
23963
24091
  const arg = args[index];
23964
24092
  if (arg === "--out" && args[index + 1]) {
23965
24093
  outPath = (0, import_node_path14.resolve)(args[++index]);
23966
24094
  }
24095
+ if (arg === "--revision") {
24096
+ const rawVersion = args[++index];
24097
+ const version = Number(rawVersion);
24098
+ if (!rawVersion || !Number.isInteger(version) || version < 1) {
24099
+ console.error("--revision must be a positive integer.");
24100
+ return 2;
24101
+ }
24102
+ requestedVersion = version;
24103
+ }
24104
+ }
24105
+ if (requestedVersion !== null) {
24106
+ sourceSelector = `version:${requestedVersion}`;
24107
+ }
24108
+ if (requestedRevisionFlags > 0 && !sourceOutput && !outPath) {
24109
+ console.error(
24110
+ "--working, --live, and --revision only apply when reading source with --source or --out."
24111
+ );
24112
+ return 2;
23967
24113
  }
23968
24114
  const playName = isFileTarget(target) ? extractPlayName((0, import_node_fs12.readFileSync)((0, import_node_path14.resolve)(target), "utf-8"), (0, import_node_path14.resolve)(target)) : parseReferencedPlayTarget2(target).playName;
23969
- const detail = isFileTarget(target) ? await client2.getPlay(playName) : await assertCanonicalNamedPlayReference(client2, target, {
23970
- command: "get"
23971
- });
23972
- const resolvedSource = detail.play.workingRevision?.sourceCode ?? detail.play.liveRevision?.sourceCode ?? detail.play.currentRevision?.sourceCode ?? detail.play.sourceCode ?? "";
23973
- const materializedFile = outPath ? materializeRemotePlaySource({
23974
- target,
24115
+ const includeSource = sourceOutput || outPath !== null;
24116
+ const detail = isFileTarget(target) ? await client2.getPlay(
23975
24117
  playName,
23976
- sourceCode: resolvedSource,
23977
- outPath
23978
- }) : null;
24118
+ includeSource ? { source: sourceSelector } : void 0
24119
+ ) : await assertCanonicalNamedPlayReference(client2, target, {
24120
+ command: "get",
24121
+ ...includeSource ? { source: sourceSelector } : {}
24122
+ });
24123
+ const source = detail.source;
24124
+ const resolvedSource = source?.files[source.entryFile] ?? "";
24125
+ let materializedFile = null;
24126
+ try {
24127
+ materializedFile = outPath && source ? materializeRemotePlaySource({ target, playName, source, outPath }) : null;
24128
+ } catch (error) {
24129
+ console.error(error instanceof Error ? error.message : String(error));
24130
+ return 2;
24131
+ }
23979
24132
  const loadedMessage = materializedFile ? formatLoadedPlayMessage(materializedFile) : null;
23980
24133
  if (jsonOutput) {
23981
24134
  process.stdout.write(
@@ -23996,14 +24149,20 @@ async function handlePlayGet(args) {
23996
24149
  return 0;
23997
24150
  }
23998
24151
  if (sourceOutput) {
23999
- if (!resolvedSource.trim()) {
24152
+ if (!source || !resolvedSource.trim()) {
24000
24153
  console.error(`No source code available for ${playName}.`);
24001
- return 1;
24154
+ return 4;
24002
24155
  }
24003
24156
  if (materializedFile) {
24004
24157
  console.log(loadedMessage);
24005
24158
  return 0;
24006
24159
  }
24160
+ if (Object.keys(source.files).length > 1) {
24161
+ console.error(
24162
+ `Play ${playName} has ${Object.keys(source.files).length} source files. Use --out <directory> to write the complete bundle.`
24163
+ );
24164
+ return 2;
24165
+ }
24007
24166
  process.stdout.write(resolvedSource);
24008
24167
  if (!resolvedSource.endsWith("\n")) {
24009
24168
  process.stdout.write("\n");
@@ -24073,7 +24232,12 @@ async function handlePlayVersions(args) {
24073
24232
  async function handlePlayList(args) {
24074
24233
  const jsonOutput = argsWantJson(args);
24075
24234
  const client2 = new DeeplineClient();
24076
- const plays = await client2.listPlays();
24235
+ const categoriesIndex = args.indexOf("--categories");
24236
+ const categories = categoriesIndex >= 0 ? args[categoriesIndex + 1]?.trim() : void 0;
24237
+ const plays = await client2.listPlays({
24238
+ categories,
24239
+ includeToolCategories: true
24240
+ });
24077
24241
  if (jsonOutput) {
24078
24242
  process.stdout.write(`${JSON.stringify(plays)}
24079
24243
  `);
@@ -24084,6 +24248,8 @@ async function handlePlayList(args) {
24084
24248
  `);
24085
24249
  for (const play of plays) {
24086
24250
  const flags = [
24251
+ play.pinned ? "pinned" : null,
24252
+ ...(play.toolCategories ?? []).map((category) => `category:${category}`),
24087
24253
  play.origin === "prebuilt" || play.ownerType === "deepline" ? "prebuilt" : "owned",
24088
24254
  play.canEdit ? "editable" : "readonly",
24089
24255
  play.isDraftDirty ? "draft-dirty" : null
@@ -24113,6 +24279,36 @@ async function handlePlayList(args) {
24113
24279
  }
24114
24280
  return 0;
24115
24281
  }
24282
+ async function handlePlayPin(args, pinned) {
24283
+ const target = args.find((arg) => !arg.startsWith("-"))?.trim();
24284
+ const jsonOutput = argsWantJson(args);
24285
+ const dryRun = args.includes("--dry-run");
24286
+ if (!target) {
24287
+ console.error(
24288
+ `Usage: deepline plays ${pinned ? "pin" : "unpin"} <play> [--dry-run] [--json]`
24289
+ );
24290
+ return 2;
24291
+ }
24292
+ const parsedTarget = parseReferencedPlayTarget2(target);
24293
+ if (parsedTarget.ownerSlug) {
24294
+ console.error(
24295
+ `deepline plays ${pinned ? "pin" : "unpin"} accepts an org-owned Play identifier without a namespace. Use "${parsedTarget.unqualifiedPlayName}" instead of "${target}".`
24296
+ );
24297
+ return 2;
24298
+ }
24299
+ const name = parsedTarget.playName;
24300
+ const plan = { name, pinned, dryRun };
24301
+ if (dryRun) {
24302
+ process.stdout.write(`${JSON.stringify(plan)}
24303
+ `);
24304
+ return 0;
24305
+ }
24306
+ const result = await new DeeplineClient().setPlayPinned(name, pinned);
24307
+ if (jsonOutput) process.stdout.write(`${JSON.stringify(result)}
24308
+ `);
24309
+ else console.log(`${result.pinned ? "Pinned" : "Unpinned"} ${result.name}.`);
24310
+ return 0;
24311
+ }
24116
24312
  function parsePlaySearchOptions(args) {
24117
24313
  const query = args[0]?.trim();
24118
24314
  if (!query) {
@@ -24147,6 +24343,12 @@ function printPlayDescription(play) {
24147
24343
  if (play.description) {
24148
24344
  console.log(` Description: ${play.description}`);
24149
24345
  }
24346
+ if (play.pinned) {
24347
+ console.log(" Pinned: yes");
24348
+ }
24349
+ if (play.toolCategories?.length) {
24350
+ console.log(` Tool categories: ${play.toolCategories.join(", ")}`);
24351
+ }
24150
24352
  if (play.aliases.length > 0) {
24151
24353
  console.log(` Aliases: ${play.aliases.join(", ")}`);
24152
24354
  }
@@ -24252,6 +24454,8 @@ function summarizePlayListItemForCli(play, options) {
24252
24454
  name: play.name,
24253
24455
  ...play.reference ? { reference: play.reference } : {},
24254
24456
  ...play.displayName ? { displayName: play.displayName } : {},
24457
+ pinned: Boolean(play.pinned),
24458
+ toolCategories: play.toolCategories ?? [],
24255
24459
  origin: play.origin,
24256
24460
  ownerType: play.ownerType,
24257
24461
  canEdit: play.canEdit,
@@ -24382,6 +24586,7 @@ async function handlePlayGrep(args) {
24382
24586
  origin: play.origin,
24383
24587
  ownerType: play.ownerType,
24384
24588
  aliases: play.aliases,
24589
+ toolCategories: play.toolCategories,
24385
24590
  inputSchema: play.inputSchema,
24386
24591
  outputSchema: play.outputSchema
24387
24592
  },
@@ -24479,6 +24684,128 @@ async function handlePlayDescribe(args) {
24479
24684
  printPlayDescription(play);
24480
24685
  return 0;
24481
24686
  }
24687
+ async function handlePlaySave(args) {
24688
+ let options;
24689
+ try {
24690
+ options = parsePlaySaveOptions(args);
24691
+ } catch (error) {
24692
+ console.error(error instanceof Error ? error.message : String(error));
24693
+ return 2;
24694
+ }
24695
+ if (!isFileTarget(options.target)) {
24696
+ console.error(
24697
+ "plays save requires a local .play.ts file. Use plays publish <play-name> to promote an existing saved revision."
24698
+ );
24699
+ return 2;
24700
+ }
24701
+ let graph;
24702
+ try {
24703
+ graph = await traceCliSpan(
24704
+ "cli.play_save_bundle_graph",
24705
+ { targetKind: "file" },
24706
+ () => collectBundledPlayGraph((0, import_node_path14.resolve)(options.target))
24707
+ );
24708
+ assertBundledPlayGraphDescriptions(graph);
24709
+ } catch (error) {
24710
+ console.error(error instanceof Error ? error.message : String(error));
24711
+ return 7;
24712
+ }
24713
+ const playName = graph.root.playName ?? extractPlayName(graph.root.sourceCode, graph.root.filePath);
24714
+ const artifactHash = graph.root.artifact.artifactHash;
24715
+ if (options.expectedArtifactHash && options.expectedArtifactHash !== artifactHash) {
24716
+ const result = {
24717
+ ok: false,
24718
+ code: "PLAY_ARTIFACT_HASH_MISMATCH",
24719
+ message: "Refusing to save: the local file no longer produces the artifact checked earlier.",
24720
+ expectedArtifactHash: options.expectedArtifactHash,
24721
+ currentArtifactHash: artifactHash,
24722
+ next: `deepline plays check ${shellQuote2(options.target)}`
24723
+ };
24724
+ if (options.jsonOutput) {
24725
+ process.stdout.write(`${JSON.stringify(result)}
24726
+ `);
24727
+ } else {
24728
+ console.error(result.message);
24729
+ console.error(` checked artifact: ${result.expectedArtifactHash}`);
24730
+ console.error(` current artifact: ${result.currentArtifactHash}`);
24731
+ console.error(` run: ${result.next}`);
24732
+ }
24733
+ return 7;
24734
+ }
24735
+ const dryRun = {
24736
+ ok: true,
24737
+ dryRun: true,
24738
+ name: playName,
24739
+ sourcePath: graph.root.filePath,
24740
+ sourceHash: graph.root.artifact.sourceHash,
24741
+ artifactHash,
24742
+ graphHash: graph.root.artifact.graphHash,
24743
+ sourceFileCount: Object.keys(graph.root.sourceFiles).length,
24744
+ plannedMutation: "save or update the mutable working draft; does not change the live revision"
24745
+ };
24746
+ if (options.dryRun) {
24747
+ if (options.jsonOutput) {
24748
+ process.stdout.write(`${JSON.stringify(dryRun)}
24749
+ `);
24750
+ } else {
24751
+ console.log(`Dry run: ${playName}`);
24752
+ console.log(` source: ${dryRun.sourceHash}`);
24753
+ console.log(` artifact: ${dryRun.artifactHash}`);
24754
+ console.log(` would: ${dryRun.plannedMutation}`);
24755
+ }
24756
+ return 0;
24757
+ }
24758
+ const client2 = new DeeplineClient();
24759
+ try {
24760
+ await traceCliSpan(
24761
+ "cli.play_save_compile_manifests",
24762
+ { targetKind: "file", nodeCount: graph.nodes.size },
24763
+ () => compileBundledPlayGraphManifests(client2, graph)
24764
+ );
24765
+ const saved = await traceCliSpan(
24766
+ "cli.play_save_register_draft",
24767
+ { targetKind: "file", playName, artifactHash },
24768
+ () => client2.registerPlayArtifact({
24769
+ name: playName,
24770
+ sourceCode: graph.root.sourceCode,
24771
+ sourceFiles: graph.root.sourceFiles,
24772
+ description: graph.root.playDescription ?? void 0,
24773
+ artifact: graph.root.artifact,
24774
+ compilerManifest: requireCompilerManifest(graph.root),
24775
+ publish: false
24776
+ })
24777
+ );
24778
+ const result = {
24779
+ success: true,
24780
+ name: playName,
24781
+ draft: true,
24782
+ revisionId: saved.revisionId ?? null,
24783
+ version: saved.version ?? null,
24784
+ sourceHash: graph.root.artifact.sourceHash,
24785
+ artifactHash,
24786
+ graphHash: graph.root.artifact.graphHash,
24787
+ artifactStorageKey: saved.artifactStorageKey,
24788
+ next: {
24789
+ publish: `deepline plays publish ${shellQuote2(options.target)} --expected-artifact ${artifactHash}`,
24790
+ get: `deepline plays get ${playName} --source --out ./${playName}/ --working`
24791
+ }
24792
+ };
24793
+ if (options.jsonOutput) {
24794
+ process.stdout.write(`${JSON.stringify(result)}
24795
+ `);
24796
+ } else {
24797
+ console.log(
24798
+ `\u2713 Saved ${playName} working draft as v${result.version ?? "?"}`
24799
+ );
24800
+ console.log(" Live revision unchanged.");
24801
+ console.log(` publish: ${result.next.publish}`);
24802
+ }
24803
+ return 0;
24804
+ } catch (error) {
24805
+ console.error(error instanceof Error ? error.message : String(error));
24806
+ return 5;
24807
+ }
24808
+ }
24482
24809
  async function handlePlayPublish(args) {
24483
24810
  let options;
24484
24811
  try {
@@ -25009,7 +25336,9 @@ Pass-through input flags:
25009
25336
  ]);
25010
25337
  });
25011
25338
  registerPlayBootstrapCommand(play);
25012
- play.command("get <target>").description("Fetch full play details.").addHelpText(
25339
+ play.command("get <target>").description(
25340
+ "Fetch full play details or materialize a saved source bundle."
25341
+ ).addHelpText(
25013
25342
  "after",
25014
25343
  `
25015
25344
  Notes:
@@ -25021,17 +25350,24 @@ Examples:
25021
25350
  deepline plays get prebuilt/person-linkedin-to-email
25022
25351
  deepline plays get prebuilt/person-linkedin-to-email --json | jq '.play.liveRevision'
25023
25352
  deepline plays get prebuilt/name-and-domain-to-email-waterfall-batch --source > email-waterfall.play.ts
25024
- deepline plays get prebuilt/name-and-domain-to-email-waterfall-batch --source --out ./email-waterfall.play.ts
25353
+ deepline plays get my-play --source --out ./my-play/ --working
25354
+ deepline plays get my-play --source --out ./my-play-v3/ --revision 3
25025
25355
  `
25026
25356
  ).option("--json", "Emit JSON output. Also automatic when stdout is piped").option(
25027
25357
  "--source",
25028
- "Print raw source code; combine with --out to write a file"
25029
- ).option("--out <path>", "Write source to a specific path").action(async (target, options) => {
25358
+ "Read source; prints single-file Plays or combines with --out to write the full bundle"
25359
+ ).option(
25360
+ "--out <path>",
25361
+ "Write source to a file or, for multi-file Plays, a directory"
25362
+ ).option("--working", "Read the current working draft source (default)").option("--live", "Read the live revision source").option("--revision <number>", "Read a specific revision source").action(async (target, options) => {
25030
25363
  process.exitCode = await handlePlayGet([
25031
25364
  target,
25032
25365
  ...options.json ? ["--json"] : [],
25033
25366
  ...options.source ? ["--source"] : [],
25034
- ...options.out ? ["--out", options.out] : []
25367
+ ...options.out ? ["--out", options.out] : [],
25368
+ ...options.working ? ["--working"] : [],
25369
+ ...options.live ? ["--live"] : [],
25370
+ ...options.revision ? ["--revision", options.revision] : []
25035
25371
  ]);
25036
25372
  });
25037
25373
  play.command("list").description("List saved and prebuilt plays.").addHelpText(
@@ -25045,11 +25381,35 @@ Examples:
25045
25381
  deepline plays list
25046
25382
  deepline plays search email --json
25047
25383
  `
25048
- ).option("--json", "Emit JSON output. Also automatic when stdout is piped").action(async (options) => {
25384
+ ).option("--json", "Emit JSON output. Also automatic when stdout is piped").option(
25385
+ "--categories <categories>",
25386
+ "Filter by comma-separated canonical tool categories"
25387
+ ).action(async (options) => {
25049
25388
  process.exitCode = await handlePlayList([
25389
+ ...options.categories ? ["--categories", options.categories] : [],
25050
25390
  ...options.json ? ["--json"] : []
25051
25391
  ]);
25052
25392
  });
25393
+ play.command("pin <target>").description("Pin an org-owned play to the top of play listings.").option("--dry-run", "Print the planned change without saving it").option("--json", "Emit JSON output").action(async (target, options) => {
25394
+ process.exitCode = await handlePlayPin(
25395
+ [
25396
+ target,
25397
+ ...options.dryRun ? ["--dry-run"] : [],
25398
+ ...options.json ? ["--json"] : []
25399
+ ],
25400
+ true
25401
+ );
25402
+ });
25403
+ play.command("unpin <target>").description("Remove an org-owned play from the pinned group.").option("--dry-run", "Print the planned change without saving it").option("--json", "Emit JSON output").action(async (target, options) => {
25404
+ process.exitCode = await handlePlayPin(
25405
+ [
25406
+ target,
25407
+ ...options.dryRun ? ["--dry-run"] : [],
25408
+ ...options.json ? ["--json"] : []
25409
+ ],
25410
+ false
25411
+ );
25412
+ });
25053
25413
  const addPlaySearchCommand = (command) => command.description("Search Deepline prebuilt plays by task.").option(
25054
25414
  "--prebuilt",
25055
25415
  "Only show Deepline-managed prebuilt plays (default)"
@@ -25142,6 +25502,32 @@ Examples:
25142
25502
  ...options.json ? ["--json"] : []
25143
25503
  ]);
25144
25504
  });
25505
+ play.command("save <file>").description(
25506
+ "Save or update a local Play working draft without publishing it."
25507
+ ).addHelpText(
25508
+ "after",
25509
+ `
25510
+ Notes:
25511
+ Mutates cloud state, but does not change the live revision or active triggers.
25512
+ Repeated saves update one working draft. Publishing it, or running a named
25513
+ draft revision, freezes it; the next save starts a new revision.
25514
+
25515
+ Examples:
25516
+ deepline plays save ./company-research.play.ts
25517
+ deepline plays save ./company-research.play.ts --dry-run --json
25518
+ deepline plays publish ./company-research.play.ts
25519
+ `
25520
+ ).option("--expected-artifact <hash>", "Require the checked artifact hash").option(
25521
+ "--dry-run",
25522
+ "Bundle and show the planned draft save without mutation"
25523
+ ).option("--json", "Emit JSON output. Also automatic when stdout is piped").action(async (file, options) => {
25524
+ process.exitCode = await handlePlaySave([
25525
+ file,
25526
+ ...options.expectedArtifact ? ["--expected-artifact", options.expectedArtifact] : [],
25527
+ ...options.dryRun ? ["--dry-run"] : [],
25528
+ ...options.json ? ["--json"] : []
25529
+ ]);
25530
+ });
25145
25531
  const addPublishHelp = (command) => command.addHelpText(
25146
25532
  "after",
25147
25533
  `
@@ -26404,10 +26790,12 @@ function compileEnrichConfigToPlaySource(config, options = {}) {
26404
26790
  const metadataColumnSource = renderMetadataColumnStep(config);
26405
26791
  const generatedAliases = collectGeneratedAliases(config.commands);
26406
26792
  const runOptionsSource = options.failFast ? `{ key: (row, index) => __dlEnrichRowKey(row, index + rowStart), onRowError: 'fail' as const }` : `{ key: (row, index) => __dlEnrichRowKey(row, index + rowStart) }`;
26793
+ const describedColumns = config.commands.filter((command) => isWaterfall(command) || !command.disabled).map(
26794
+ (command) => (isWaterfall(command) ? command.with_waterfall : command.alias).replace(/[_-]+/g, " ").trim()
26795
+ ).filter(Boolean).slice(0, 3);
26796
+ const generatedDescription = describedColumns.length ? `Enrich CSV rows with ${describedColumns.join(", ")}.` : "Prepare CSV rows for enrichment.";
26407
26797
  const playOptionsSource = [
26408
- `description: ${stringLiteral(
26409
- "Read a CSV file, run the configured Deepline enrich commands, and return enriched rows."
26410
- )}`,
26798
+ `description: ${stringLiteral(generatedDescription)}`,
26411
26799
  ...options.maxCreditsPerRun === void 0 ? [] : [`billing: { maxCreditsPerRun: ${String(options.maxCreditsPerRun)} }`]
26412
26800
  ].join(", ");
26413
26801
  const body = [