@sdt-tools/cli 0.2.0 → 0.2.6

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.
Files changed (207) hide show
  1. package/dist/advise-tests-6DRSZMBL.js +87 -0
  2. package/dist/advise-tests-6DRSZMBL.js.map +1 -0
  3. package/dist/ai-G4MJWHTM.js +89 -0
  4. package/dist/ai-G4MJWHTM.js.map +1 -0
  5. package/dist/anonymize-QR6JGXA7.js +123 -0
  6. package/dist/anonymize-QR6JGXA7.js.map +1 -0
  7. package/dist/approval-YVHYTV53.js +73 -0
  8. package/dist/approval-YVHYTV53.js.map +1 -0
  9. package/dist/approval-chain-54KKJZS3.js +120 -0
  10. package/dist/approval-chain-54KKJZS3.js.map +1 -0
  11. package/dist/audit-log-QZFH7LUX.js +159 -0
  12. package/dist/audit-log-QZFH7LUX.js.map +1 -0
  13. package/dist/backlog-V2YUIQDL.js +76 -0
  14. package/dist/backlog-V2YUIQDL.js.map +1 -0
  15. package/dist/bisect-GEVYAVL5.js +111 -0
  16. package/dist/bisect-GEVYAVL5.js.map +1 -0
  17. package/dist/bookmarks-57LKS7P6.js +107 -0
  18. package/dist/bookmarks-57LKS7P6.js.map +1 -0
  19. package/dist/branch-W2MGMPSH.js +88 -0
  20. package/dist/branch-W2MGMPSH.js.map +1 -0
  21. package/dist/build-VNIQFKSP.js +23 -0
  22. package/dist/build-VNIQFKSP.js.map +1 -0
  23. package/dist/catalog-JLB5VCEV.js +137 -0
  24. package/dist/catalog-JLB5VCEV.js.map +1 -0
  25. package/dist/changelog-M7XGDYSY.js +220 -0
  26. package/dist/changelog-M7XGDYSY.js.map +1 -0
  27. package/dist/chunk-DGUM43GV.js +11 -0
  28. package/dist/chunk-DGUM43GV.js.map +1 -0
  29. package/dist/chunk-EWXM4KJN.js +25 -0
  30. package/dist/chunk-EWXM4KJN.js.map +1 -0
  31. package/dist/chunk-JP2EZLR5.js +50 -0
  32. package/dist/chunk-JP2EZLR5.js.map +1 -0
  33. package/dist/chunk-VM2H4LAO.js +15 -0
  34. package/dist/chunk-VM2H4LAO.js.map +1 -0
  35. package/dist/chunk-ZWY4ZRHL.js +44 -0
  36. package/dist/chunk-ZWY4ZRHL.js.map +1 -0
  37. package/dist/cli.js +511 -19014
  38. package/dist/cli.js.map +1 -1
  39. package/dist/compare-5O6UTWPJ.js +405 -0
  40. package/dist/compare-5O6UTWPJ.js.map +1 -0
  41. package/dist/compare-profiles-7ZSNIW7B.js +218 -0
  42. package/dist/compare-profiles-7ZSNIW7B.js.map +1 -0
  43. package/dist/completion-I5U5VVAX.js +82 -0
  44. package/dist/completion-I5U5VVAX.js.map +1 -0
  45. package/dist/connection-GNTZDHXF.js +133 -0
  46. package/dist/connection-GNTZDHXF.js.map +1 -0
  47. package/dist/cost-estimate-TJDDH6TO.js +328 -0
  48. package/dist/cost-estimate-TJDDH6TO.js.map +1 -0
  49. package/dist/data-compare-UK2UXAS3.js +134 -0
  50. package/dist/data-compare-UK2UXAS3.js.map +1 -0
  51. package/dist/data-fit-Q45ENBRL.js +125 -0
  52. package/dist/data-fit-Q45ENBRL.js.map +1 -0
  53. package/dist/deploy-status-UUHKVDTI.js +58 -0
  54. package/dist/deploy-status-UUHKVDTI.js.map +1 -0
  55. package/dist/design-PO6UPBL7.js +138 -0
  56. package/dist/design-PO6UPBL7.js.map +1 -0
  57. package/dist/diagnose-6IFMELFR.js +145 -0
  58. package/dist/diagnose-6IFMELFR.js.map +1 -0
  59. package/dist/discover-A7OSZAHK.js +78 -0
  60. package/dist/discover-A7OSZAHK.js.map +1 -0
  61. package/dist/docs-CVRKGUSW.js +177 -0
  62. package/dist/docs-CVRKGUSW.js.map +1 -0
  63. package/dist/drift-XDA3BDYN.js +226 -0
  64. package/dist/drift-XDA3BDYN.js.map +1 -0
  65. package/dist/drift-gate-V7QSIOGZ.js +94 -0
  66. package/dist/drift-gate-V7QSIOGZ.js.map +1 -0
  67. package/dist/error-lookup-7ZWCZJ44.js +56 -0
  68. package/dist/error-lookup-7ZWCZJ44.js.map +1 -0
  69. package/dist/errorReporting-AQXKKGZH.js +109 -0
  70. package/dist/errorReporting-AQXKKGZH.js.map +1 -0
  71. package/dist/exec-PKBHLI7T.js +121 -0
  72. package/dist/exec-PKBHLI7T.js.map +1 -0
  73. package/dist/explain-LWKJOTL7.js +192 -0
  74. package/dist/explain-LWKJOTL7.js.map +1 -0
  75. package/dist/explorer-QOVM6VBD.js +61 -0
  76. package/dist/explorer-QOVM6VBD.js.map +1 -0
  77. package/dist/export-IYYBZ5HE.js +42 -0
  78. package/dist/export-IYYBZ5HE.js.map +1 -0
  79. package/dist/extract-VMMVRQVT.js +102 -0
  80. package/dist/extract-VMMVRQVT.js.map +1 -0
  81. package/dist/features-LE6BDZ2S.js +59 -0
  82. package/dist/features-LE6BDZ2S.js.map +1 -0
  83. package/dist/feedback-M7DM2EQC.js +161 -0
  84. package/dist/feedback-M7DM2EQC.js.map +1 -0
  85. package/dist/find-EME2JG2I.js +176 -0
  86. package/dist/find-EME2JG2I.js.map +1 -0
  87. package/dist/format-TRLWLMGS.js +141 -0
  88. package/dist/format-TRLWLMGS.js.map +1 -0
  89. package/dist/generate-6NAZGZDV.js +152 -0
  90. package/dist/generate-6NAZGZDV.js.map +1 -0
  91. package/dist/graph-QNQDAUO7.js +161 -0
  92. package/dist/graph-QNQDAUO7.js.map +1 -0
  93. package/dist/history-RONA7ZTI.js +199 -0
  94. package/dist/history-RONA7ZTI.js.map +1 -0
  95. package/dist/hosts-YBXY2ZG5.js +49 -0
  96. package/dist/hosts-YBXY2ZG5.js.map +1 -0
  97. package/dist/impact-T2JSANHS.js +59 -0
  98. package/dist/impact-T2JSANHS.js.map +1 -0
  99. package/dist/import-AELYLY6A.js +32 -0
  100. package/dist/import-AELYLY6A.js.map +1 -0
  101. package/dist/import-script-2OF5BI6A.js +83 -0
  102. package/dist/import-script-2OF5BI6A.js.map +1 -0
  103. package/dist/index.cjs +71 -12
  104. package/dist/index.cjs.map +1 -1
  105. package/dist/index.js +95 -31
  106. package/dist/index.js.map +1 -1
  107. package/dist/init-SWRRJMGI.js +57 -0
  108. package/dist/init-SWRRJMGI.js.map +1 -0
  109. package/dist/install-hooks-6SIAGTAF.js +109 -0
  110. package/dist/install-hooks-6SIAGTAF.js.map +1 -0
  111. package/dist/license-OAF22PLZ.js +46 -0
  112. package/dist/license-OAF22PLZ.js.map +1 -0
  113. package/dist/lineage-EW66XJ6O.js +552 -0
  114. package/dist/lineage-EW66XJ6O.js.map +1 -0
  115. package/dist/lint-FQ2OTYTQ.js +143 -0
  116. package/dist/lint-FQ2OTYTQ.js.map +1 -0
  117. package/dist/mcp-SARDMCDV.js +344 -0
  118. package/dist/mcp-SARDMCDV.js.map +1 -0
  119. package/dist/migrate-from-dbt-JVTXPWKQ.js +156 -0
  120. package/dist/migrate-from-dbt-JVTXPWKQ.js.map +1 -0
  121. package/dist/migrate-platform-NTRTOGNR.js +91 -0
  122. package/dist/migrate-platform-NTRTOGNR.js.map +1 -0
  123. package/dist/optimize-CJYWMAWA.js +105 -0
  124. package/dist/optimize-CJYWMAWA.js.map +1 -0
  125. package/dist/perf-LL2CPCJF.js +205 -0
  126. package/dist/perf-LL2CPCJF.js.map +1 -0
  127. package/dist/pii-FBDRDQ2E.js +136 -0
  128. package/dist/pii-FBDRDQ2E.js.map +1 -0
  129. package/dist/pilot-CCQERKPH.js +29 -0
  130. package/dist/pilot-CCQERKPH.js.map +1 -0
  131. package/dist/pr-comment-S5FF4QRX.js +79 -0
  132. package/dist/pr-comment-S5FF4QRX.js.map +1 -0
  133. package/dist/preview-5U4YVCRM.js +47 -0
  134. package/dist/preview-5U4YVCRM.js.map +1 -0
  135. package/dist/profile-7VC57KD2.js +101 -0
  136. package/dist/profile-7VC57KD2.js.map +1 -0
  137. package/dist/promote-AASEFTIA.js +408 -0
  138. package/dist/promote-AASEFTIA.js.map +1 -0
  139. package/dist/publish-UMVIWH6H.js +721 -0
  140. package/dist/publish-UMVIWH6H.js.map +1 -0
  141. package/dist/purge-QMXZKCMD.js +57 -0
  142. package/dist/purge-QMXZKCMD.js.map +1 -0
  143. package/dist/query-log-6OM4GI7W.js +112 -0
  144. package/dist/query-log-6OM4GI7W.js.map +1 -0
  145. package/dist/refactor-LTZQLJ35.js +5799 -0
  146. package/dist/refactor-LTZQLJ35.js.map +1 -0
  147. package/dist/refresh-4TY2AGOU.js +38 -0
  148. package/dist/refresh-4TY2AGOU.js.map +1 -0
  149. package/dist/replay-OOC25FZN.js +117 -0
  150. package/dist/replay-OOC25FZN.js.map +1 -0
  151. package/dist/revert-ODMUVJW6.js +110 -0
  152. package/dist/revert-ODMUVJW6.js.map +1 -0
  153. package/dist/review-XXPWOBFP.js +158 -0
  154. package/dist/review-XXPWOBFP.js.map +1 -0
  155. package/dist/rollback-suggest-6G2HEKFR.js +79 -0
  156. package/dist/rollback-suggest-6G2HEKFR.js.map +1 -0
  157. package/dist/safer-alternative-QFVNLG3L.js +89 -0
  158. package/dist/safer-alternative-QFVNLG3L.js.map +1 -0
  159. package/dist/safety-7QWRSUEZ.js +168 -0
  160. package/dist/safety-7QWRSUEZ.js.map +1 -0
  161. package/dist/savings-RHIXP6IT.js +95 -0
  162. package/dist/savings-RHIXP6IT.js.map +1 -0
  163. package/dist/scan-secrets-5YCQ4UCU.js +54 -0
  164. package/dist/scan-secrets-5YCQ4UCU.js.map +1 -0
  165. package/dist/schema-CIZXCQD2.js +429 -0
  166. package/dist/schema-CIZXCQD2.js.map +1 -0
  167. package/dist/script-K7CIN2P6.js +153 -0
  168. package/dist/script-K7CIN2P6.js.map +1 -0
  169. package/dist/search-BUZ5NXZZ.js +151 -0
  170. package/dist/search-BUZ5NXZZ.js.map +1 -0
  171. package/dist/seed-76QAK276.js +96 -0
  172. package/dist/seed-76QAK276.js.map +1 -0
  173. package/dist/sketch-PTLKDIK3.js +88 -0
  174. package/dist/sketch-PTLKDIK3.js.map +1 -0
  175. package/dist/snapshot-XLPR2OZ5.js +177 -0
  176. package/dist/snapshot-XLPR2OZ5.js.map +1 -0
  177. package/dist/snippets-EK4DK5CN.js +74 -0
  178. package/dist/snippets-EK4DK5CN.js.map +1 -0
  179. package/dist/standards-7T2UY6DD.js +241 -0
  180. package/dist/standards-7T2UY6DD.js.map +1 -0
  181. package/dist/suggest-VGRYSAR6.js +39 -0
  182. package/dist/suggest-VGRYSAR6.js.map +1 -0
  183. package/dist/suggest-constraints-MY5WKUHA.js +160 -0
  184. package/dist/suggest-constraints-MY5WKUHA.js.map +1 -0
  185. package/dist/suite-TRNGZWQM.js +88 -0
  186. package/dist/suite-TRNGZWQM.js.map +1 -0
  187. package/dist/telemetry-3U2QLA2S.js +75 -0
  188. package/dist/telemetry-3U2QLA2S.js.map +1 -0
  189. package/dist/template-ZERIXVXF.js +403 -0
  190. package/dist/template-ZERIXVXF.js.map +1 -0
  191. package/dist/test-5M2ED3WT.js +169 -0
  192. package/dist/test-5M2ED3WT.js.map +1 -0
  193. package/dist/trial-U732FONV.js +31 -0
  194. package/dist/trial-U732FONV.js.map +1 -0
  195. package/dist/validate-T6D2WCOK.js +106 -0
  196. package/dist/validate-T6D2WCOK.js.map +1 -0
  197. package/dist/verify-KXVASEEG.js +76 -0
  198. package/dist/verify-KXVASEEG.js.map +1 -0
  199. package/dist/watch-I6K4BNMA.js +80 -0
  200. package/dist/watch-I6K4BNMA.js.map +1 -0
  201. package/dist/xcompare-TPFLQO6W.js +87 -0
  202. package/dist/xcompare-TPFLQO6W.js.map +1 -0
  203. package/package.json +2 -2
  204. package/dist/cli.cjs +0 -19040
  205. package/dist/cli.cjs.map +0 -1
  206. package/dist/cli.d.cts +0 -1
  207. package/dist/cli.d.ts +0 -1
@@ -0,0 +1,102 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-VM2H4LAO.js";
4
+ import "./chunk-DGUM43GV.js";
5
+
6
+ // src/commands/extract.ts
7
+ import path from "path";
8
+ import { promises as fs } from "fs";
9
+ import { Command } from "commander";
10
+ import { catalog } from "@sdt-tools/core";
11
+ import { getProfile, SnowflakeConnection } from "@sdt-tools/core/connection";
12
+ import { AccountExtractor, defaultExtractors } from "@sdt-tools/core/extract";
13
+ import { writeProjectTree } from "@sdt-tools/core/project";
14
+ function extractCommand() {
15
+ const cmd = new Command("extract");
16
+ cmd.description("Extract a Snowflake account/database/schema into a project layout.").requiredOption("-c, --connection <profile>", "Connection profile name").requiredOption(
17
+ "-o, --output <path>",
18
+ "Target project directory (must contain a .sdtproj or pass --init)"
19
+ ).option("--db <database>", "Limit to a single database").option("--schema <schema>", "Limit to a single schema (requires --db)").option("--types <list>", "Comma-separated object types to include (default: all implemented)").option(
20
+ "--write-catalog-cache",
21
+ "Also write the extracted model to the EE1 catalog cache (.sdt/cache/<connection>/catalog.msgpack) so subsequent editor sessions have a populated cache without an extra `catalog refresh` round-trip.",
22
+ false
23
+ ).option(
24
+ "--use-catalog-cache",
25
+ "Read object metadata from the on-disk EE1 catalog cache instead of querying Snowflake. Fast / offline \u2014 no live connection required. Writes a catalog-snapshot.json to the output dir. Requires a prior `sdt catalog refresh` or `sdt extract --write-catalog-cache` run.",
26
+ false
27
+ ).action(async (opts) => {
28
+ if (opts.useCatalogCache) {
29
+ const out = path.resolve(String(opts.output));
30
+ await fs.mkdir(out, { recursive: true });
31
+ const cache = new catalog.CatalogCache({ root: out, connection: String(opts.connection) });
32
+ const snapshot = await cache.get();
33
+ if (snapshot.databases.length === 0 && snapshot.fingerprint === null) {
34
+ logger.error(
35
+ `No catalog cache found at ${cache.path}. Run "sdt catalog refresh -c ${String(opts.connection)}" or "sdt extract --write-catalog-cache" first.`
36
+ );
37
+ process.exitCode = 1;
38
+ return;
39
+ }
40
+ const outPath = path.join(out, ".sdt-cache", "catalog-snapshot.json");
41
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
42
+ await fs.writeFile(outPath, JSON.stringify(snapshot, null, 2));
43
+ const objCount = snapshot.databases.flatMap(
44
+ (d) => d.schemas.flatMap((s) => s.objects)
45
+ ).length;
46
+ logger.success(
47
+ `Loaded ${objCount} object(s) from catalog cache (${snapshot.databases.length} database(s)) \u2192 ${outPath}`
48
+ );
49
+ logger.dim(
50
+ `Cache was snapshotted at ${snapshot.snapshotAt}. Use "sdt catalog refresh" to update it.`
51
+ );
52
+ return;
53
+ }
54
+ const profile = await getProfile(String(opts.connection));
55
+ const conn = new SnowflakeConnection(profile);
56
+ const include = opts.types ? new Set(
57
+ String(opts.types).split(",").map((s) => s.trim().toUpperCase())
58
+ ) : void 0;
59
+ logger.step(`Connecting to ${profile.account} as ${profile.auth.username}\u2026`);
60
+ await conn.connect();
61
+ try {
62
+ const account = new AccountExtractor(defaultExtractors());
63
+ const objects = await account.extract(
64
+ conn,
65
+ {
66
+ database: opts.db ? String(opts.db) : void 0,
67
+ schema: opts.schema ? String(opts.schema) : void 0
68
+ },
69
+ { include }
70
+ );
71
+ const out = path.resolve(String(opts.output));
72
+ await fs.mkdir(out, { recursive: true });
73
+ const dumpPath = path.join(out, ".sdt-cache", "extracted.json");
74
+ await fs.mkdir(path.dirname(dumpPath), { recursive: true });
75
+ await fs.writeFile(dumpPath, JSON.stringify(objects, null, 2));
76
+ const filesCreated = await writeProjectTree(out, objects);
77
+ logger.success(
78
+ `Extracted ${objects.length} object(s) \u2192 ${filesCreated} .sql file(s) under ${out} (cache: ${dumpPath}).`
79
+ );
80
+ if (opts.writeCatalogCache) {
81
+ const snapshot = catalog.snapshotFromObjects(objects, {
82
+ connection: String(opts.connection)
83
+ });
84
+ const cache = new catalog.CatalogCache({
85
+ root: out,
86
+ connection: String(opts.connection)
87
+ });
88
+ await cache.set(snapshot);
89
+ logger.success(
90
+ `Wrote catalog cache \u2192 ${cache.path} (${snapshot.databases.length} database(s), ${objects.length} object(s)).`
91
+ );
92
+ }
93
+ } finally {
94
+ await conn.disconnect();
95
+ }
96
+ });
97
+ return cmd;
98
+ }
99
+ export {
100
+ extractCommand
101
+ };
102
+ //# sourceMappingURL=extract-VMMVRQVT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/extract.ts"],"sourcesContent":["import path from 'node:path';\nimport { promises as fs } from 'node:fs';\nimport { Command } from 'commander';\nimport { catalog } from '@sdt-tools/core';\nimport { getProfile, SnowflakeConnection } from '@sdt-tools/core/connection';\nimport { AccountExtractor, defaultExtractors } from '@sdt-tools/core/extract';\nimport { writeProjectTree } from '@sdt-tools/core/project';\nimport { logger } from '../util/logger.js';\n\nexport function extractCommand(): Command {\n const cmd = new Command('extract');\n cmd\n .description('Extract a Snowflake account/database/schema into a project layout.')\n .requiredOption('-c, --connection <profile>', 'Connection profile name')\n .requiredOption(\n '-o, --output <path>',\n 'Target project directory (must contain a .sdtproj or pass --init)',\n )\n .option('--db <database>', 'Limit to a single database')\n .option('--schema <schema>', 'Limit to a single schema (requires --db)')\n .option('--types <list>', 'Comma-separated object types to include (default: all implemented)')\n .option(\n '--write-catalog-cache',\n 'Also write the extracted model to the EE1 catalog cache (.sdt/cache/<connection>/catalog.msgpack) so subsequent editor sessions have a populated cache without an extra `catalog refresh` round-trip.',\n false,\n )\n .option(\n '--use-catalog-cache',\n 'Read object metadata from the on-disk EE1 catalog cache instead of querying Snowflake. Fast / offline — no live connection required. Writes a catalog-snapshot.json to the output dir. Requires a prior `sdt catalog refresh` or `sdt extract --write-catalog-cache` run.',\n false,\n )\n .action(async (opts) => {\n if (opts.useCatalogCache) {\n const out = path.resolve(String(opts.output));\n await fs.mkdir(out, { recursive: true });\n const cache = new catalog.CatalogCache({ root: out, connection: String(opts.connection) });\n const snapshot = await cache.get();\n if (snapshot.databases.length === 0 && snapshot.fingerprint === null) {\n logger.error(\n `No catalog cache found at ${cache.path}. Run \"sdt catalog refresh -c ${String(opts.connection)}\" or \"sdt extract --write-catalog-cache\" first.`,\n );\n process.exitCode = 1;\n return;\n }\n const outPath = path.join(out, '.sdt-cache', 'catalog-snapshot.json');\n await fs.mkdir(path.dirname(outPath), { recursive: true });\n await fs.writeFile(outPath, JSON.stringify(snapshot, null, 2));\n const objCount = snapshot.databases.flatMap((d) =>\n d.schemas.flatMap((s) => s.objects),\n ).length;\n logger.success(\n `Loaded ${objCount} object(s) from catalog cache (${snapshot.databases.length} database(s)) → ${outPath}`,\n );\n logger.dim(\n `Cache was snapshotted at ${snapshot.snapshotAt}. Use \"sdt catalog refresh\" to update it.`,\n );\n return;\n }\n\n const profile = await getProfile(String(opts.connection));\n const conn = new SnowflakeConnection(profile);\n const include = opts.types\n ? new Set(\n String(opts.types)\n .split(',')\n .map((s) => s.trim().toUpperCase()),\n )\n : undefined;\n logger.step(`Connecting to ${profile.account} as ${profile.auth.username}…`);\n await conn.connect();\n try {\n const account = new AccountExtractor(defaultExtractors());\n const objects = await account.extract(\n conn,\n {\n database: opts.db ? String(opts.db) : undefined,\n schema: opts.schema ? String(opts.schema) : undefined,\n },\n { include },\n );\n const out = path.resolve(String(opts.output));\n await fs.mkdir(out, { recursive: true });\n const dumpPath = path.join(out, '.sdt-cache', 'extracted.json');\n await fs.mkdir(path.dirname(dumpPath), { recursive: true });\n await fs.writeFile(dumpPath, JSON.stringify(objects, null, 2));\n const filesCreated = await writeProjectTree(out, objects);\n logger.success(\n `Extracted ${objects.length} object(s) → ${filesCreated} .sql file(s) under ${out} (cache: ${dumpPath}).`,\n );\n\n if (opts.writeCatalogCache) {\n const snapshot = catalog.snapshotFromObjects(objects, {\n connection: String(opts.connection),\n });\n const cache = new catalog.CatalogCache({\n root: out,\n connection: String(opts.connection),\n });\n await cache.set(snapshot);\n logger.success(\n `Wrote catalog cache → ${cache.path} (${snapshot.databases.length} database(s), ${objects.length} object(s)).`,\n );\n }\n } finally {\n await conn.disconnect();\n }\n });\n return cmd;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,59 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-VM2H4LAO.js";
4
+ import "./chunk-DGUM43GV.js";
5
+
6
+ // src/commands/features.ts
7
+ import { Command } from "commander";
8
+ import { features as featuresApi, license as licenseApi } from "@sdt-tools/core";
9
+ function featuresCommand() {
10
+ const cmd = new Command("features");
11
+ cmd.description(
12
+ "Browse SDT features. Free users see paid features too \u2014 with copy explaining what each one does and how to unlock it."
13
+ );
14
+ cmd.command("list").description("List every feature with its tier and your current access state.").action(async () => {
15
+ const license = await licenseApi.loadLicense();
16
+ const rows = featuresApi.listFeatureAvailability(license);
17
+ const tierWidth = Math.max(...rows.map((r) => r.feature.tier.length), 4);
18
+ const nameWidth = Math.max(...rows.map((r) => r.feature.name.length), 4);
19
+ let lastTier = "";
20
+ for (const row of rows) {
21
+ if (row.feature.tier !== lastTier) {
22
+ if (lastTier !== "") process.stdout.write("\n");
23
+ process.stdout.write(`\u2500\u2500 ${row.feature.tier.toUpperCase()} \u2500\u2500
24
+ `);
25
+ lastTier = row.feature.tier;
26
+ }
27
+ const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
28
+ process.stdout.write(
29
+ ` ${pad(row.state, 22)} ${pad(row.feature.name, nameWidth)} ${row.feature.summary}
30
+ `
31
+ );
32
+ }
33
+ void tierWidth;
34
+ process.stdout.write(
35
+ "\nRun `sdt features show <id>` for the full pitch. `sdt features upgrade` shows the path to the next tier.\n"
36
+ );
37
+ });
38
+ cmd.command("show <id>").description("Print the full pitch + unlock path for one feature.").action(async (id) => {
39
+ const feature = featuresApi.getFeature(id);
40
+ if (!feature) {
41
+ logger.error(`Unknown feature id: "${id}". Run \`sdt features list\` to see all ids.`);
42
+ process.exitCode = 1;
43
+ return;
44
+ }
45
+ const license = await licenseApi.loadLicense();
46
+ const available = featuresApi.isFeatureAvailable(feature, license);
47
+ const body = available ? featuresApi.unlockedFeatureMessage(feature) : featuresApi.lockedFeatureMessage(feature);
48
+ process.stdout.write(body + "\n");
49
+ });
50
+ cmd.command("upgrade").description("Show what each paid tier would unlock for your current license.").action(async () => {
51
+ const license = await licenseApi.loadLicense();
52
+ process.stdout.write(featuresApi.upgradeSummary(license) + "\n");
53
+ });
54
+ return cmd;
55
+ }
56
+ export {
57
+ featuresCommand
58
+ };
59
+ //# sourceMappingURL=features-LE6BDZ2S.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/features.ts"],"sourcesContent":["/**\n * `sdt features` — browse the SDT feature catalog regardless of tier.\n *\n * The point: every user can see every feature, with copy that explains\n * what it does and exactly what would unlock it. Better UX than silent\n * gates (\"why doesn't `--map-rewrite-strings` work?\") or surprise\n * paywalls at the command line.\n *\n * Subcommands:\n * sdt features list Show all features with state (✅ / 🔒 / 🚧).\n * sdt features show <id> Full pitch + unlock path for one feature.\n * sdt features upgrade What each paid tier would unlock for you.\n */\nimport { Command } from 'commander';\nimport { features as featuresApi, license as licenseApi } from '@sdt-tools/core';\nimport { logger } from '../util/logger.js';\n\nexport function featuresCommand(): Command {\n const cmd = new Command('features');\n cmd.description(\n 'Browse SDT features. Free users see paid features too — with copy explaining what each one does and how to unlock it.',\n );\n\n cmd\n .command('list')\n .description('List every feature with its tier and your current access state.')\n .action(async () => {\n const license = await licenseApi.loadLicense();\n const rows = featuresApi.listFeatureAvailability(license);\n\n const tierWidth = Math.max(...rows.map((r) => r.feature.tier.length), 4);\n const nameWidth = Math.max(...rows.map((r) => r.feature.name.length), 4);\n\n // Group output by tier for readability.\n let lastTier = '';\n for (const row of rows) {\n if (row.feature.tier !== lastTier) {\n if (lastTier !== '') process.stdout.write('\\n');\n process.stdout.write(`── ${row.feature.tier.toUpperCase()} ──\\n`);\n lastTier = row.feature.tier;\n }\n const pad = (s: string, w: number): string => s + ' '.repeat(Math.max(0, w - s.length));\n process.stdout.write(\n ` ${pad(row.state, 22)} ${pad(row.feature.name, nameWidth)} ${row.feature.summary}\\n`,\n );\n }\n void tierWidth;\n process.stdout.write(\n '\\nRun `sdt features show <id>` for the full pitch. `sdt features upgrade` shows the path to the next tier.\\n',\n );\n });\n\n cmd\n .command('show <id>')\n .description('Print the full pitch + unlock path for one feature.')\n .action(async (id: string) => {\n const feature = featuresApi.getFeature(id);\n if (!feature) {\n logger.error(`Unknown feature id: \"${id}\". Run \\`sdt features list\\` to see all ids.`);\n process.exitCode = 1;\n return;\n }\n const license = await licenseApi.loadLicense();\n const available = featuresApi.isFeatureAvailable(feature, license);\n const body = available\n ? featuresApi.unlockedFeatureMessage(feature)\n : featuresApi.lockedFeatureMessage(feature);\n process.stdout.write(body + '\\n');\n });\n\n cmd\n .command('upgrade')\n .description('Show what each paid tier would unlock for your current license.')\n .action(async () => {\n const license = await licenseApi.loadLicense();\n process.stdout.write(featuresApi.upgradeSummary(license) + '\\n');\n });\n\n return cmd;\n}\n"],"mappings":";;;;;;AAaA,SAAS,eAAe;AACxB,SAAS,YAAY,aAAa,WAAW,kBAAkB;AAGxD,SAAS,kBAA2B;AACzC,QAAM,MAAM,IAAI,QAAQ,UAAU;AAClC,MAAI;AAAA,IACF;AAAA,EACF;AAEA,MACG,QAAQ,MAAM,EACd,YAAY,iEAAiE,EAC7E,OAAO,YAAY;AAClB,UAAM,UAAU,MAAM,WAAW,YAAY;AAC7C,UAAM,OAAO,YAAY,wBAAwB,OAAO;AAExD,UAAM,YAAY,KAAK,IAAI,GAAG,KAAK,IAAI,CAAC,MAAM,EAAE,QAAQ,KAAK,MAAM,GAAG,CAAC;AACvE,UAAM,YAAY,KAAK,IAAI,GAAG,KAAK,IAAI,CAAC,MAAM,EAAE,QAAQ,KAAK,MAAM,GAAG,CAAC;AAGvE,QAAI,WAAW;AACf,eAAW,OAAO,MAAM;AACtB,UAAI,IAAI,QAAQ,SAAS,UAAU;AACjC,YAAI,aAAa,GAAI,SAAQ,OAAO,MAAM,IAAI;AAC9C,gBAAQ,OAAO,MAAM,gBAAM,IAAI,QAAQ,KAAK,YAAY,CAAC;AAAA,CAAO;AAChE,mBAAW,IAAI,QAAQ;AAAA,MACzB;AACA,YAAM,MAAM,CAAC,GAAW,MAAsB,IAAI,IAAI,OAAO,KAAK,IAAI,GAAG,IAAI,EAAE,MAAM,CAAC;AACtF,cAAQ,OAAO;AAAA,QACb,KAAK,IAAI,IAAI,OAAO,EAAE,CAAC,KAAK,IAAI,IAAI,QAAQ,MAAM,SAAS,CAAC,KAAK,IAAI,QAAQ,OAAO;AAAA;AAAA,MACtF;AAAA,IACF;AACA,SAAK;AACL,YAAQ,OAAO;AAAA,MACb;AAAA,IACF;AAAA,EACF,CAAC;AAEH,MACG,QAAQ,WAAW,EACnB,YAAY,qDAAqD,EACjE,OAAO,OAAO,OAAe;AAC5B,UAAM,UAAU,YAAY,WAAW,EAAE;AACzC,QAAI,CAAC,SAAS;AACZ,aAAO,MAAM,wBAAwB,EAAE,8CAA8C;AACrF,cAAQ,WAAW;AACnB;AAAA,IACF;AACA,UAAM,UAAU,MAAM,WAAW,YAAY;AAC7C,UAAM,YAAY,YAAY,mBAAmB,SAAS,OAAO;AACjE,UAAM,OAAO,YACT,YAAY,uBAAuB,OAAO,IAC1C,YAAY,qBAAqB,OAAO;AAC5C,YAAQ,OAAO,MAAM,OAAO,IAAI;AAAA,EAClC,CAAC;AAEH,MACG,QAAQ,SAAS,EACjB,YAAY,iEAAiE,EAC7E,OAAO,YAAY;AAClB,UAAM,UAAU,MAAM,WAAW,YAAY;AAC7C,YAAQ,OAAO,MAAM,YAAY,eAAe,OAAO,IAAI,IAAI;AAAA,EACjE,CAAC;AAEH,SAAO;AACT;","names":[]}
@@ -0,0 +1,161 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-VM2H4LAO.js";
4
+ import "./chunk-DGUM43GV.js";
5
+
6
+ // src/commands/feedback.ts
7
+ import { promises as fs } from "fs";
8
+ import path from "path";
9
+ import os from "os";
10
+ import { Command } from "commander";
11
+ import { license } from "@sdt-tools/core";
12
+ var DEFAULT_INGEST_URL = process.env.SDT_FEEDBACK_URL ?? "https://sdt-support.sdt-ddt-tools.workers.dev/ingest";
13
+ var OUTBOX_DIR = path.join(os.homedir(), ".sdt", "feedback");
14
+ function feedbackCommand() {
15
+ const cmd = new Command("feedback");
16
+ cmd.description("Send a feedback / bug / feature-request ticket to the SDT team.").argument("<message...>", "The feedback body (quote to keep as one arg).").option("--category <kind>", "Category: bug | feature | question | docs | other", "other").option(
17
+ "--contact <email>",
18
+ "Reply-to email (optional; otherwise we use the license email if any)."
19
+ ).option("--no-context", "Skip auto-capture of CLI version / OS / recent commands.").option("--dry-run", "Print the payload without sending or queueing.").option("--url <url>", `Override the ingest endpoint. Default ${DEFAULT_INGEST_URL}.`).action(async (messageParts, opts) => {
20
+ const body = (messageParts ?? []).join(" ").trim();
21
+ if (!body) {
22
+ logger.error("Empty feedback body \u2014 nothing to send.");
23
+ process.exitCode = 1;
24
+ return;
25
+ }
26
+ const payload = await buildPayload(body, opts);
27
+ if (opts.dryRun) {
28
+ console.log(JSON.stringify(payload, null, 2));
29
+ return;
30
+ }
31
+ try {
32
+ await sendOrQueue(payload, String(opts.url ?? DEFAULT_INGEST_URL));
33
+ logger.success("Feedback sent. Thank you!");
34
+ logger.dim(" You should hear back within 1 business day for Pro+ accounts.");
35
+ if (!payload.licenseEmail) {
36
+ logger.dim(" Tip: install a license (`sdt license activate`) so we can reach you.");
37
+ }
38
+ const extended = await license.extendTrial(1);
39
+ if (extended) {
40
+ logger.dim(
41
+ ` Feedback bonus: +1 trial day (now expires ${extended.expiresAt.slice(0, 10)}).`
42
+ );
43
+ }
44
+ } catch (err) {
45
+ const message = err instanceof Error ? err.message : String(err);
46
+ logger.warn(`Couldn't reach the ingest endpoint: ${message}`);
47
+ const queued = await queueLocally(payload);
48
+ logger.dim(` Queued locally at ${queued}. Will retry on next \`sdt feedback\` run.`);
49
+ }
50
+ });
51
+ return cmd;
52
+ }
53
+ async function buildPayload(body, opts) {
54
+ const category = normalizeCategory(opts.category);
55
+ const payload = {
56
+ body,
57
+ product: "sdt",
58
+ productVersion: await readCliVersion(),
59
+ os: `${os.platform()} ${os.release()}`,
60
+ nodeVersion: process.version,
61
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
62
+ category
63
+ };
64
+ if (opts.contact) payload.contact = String(opts.contact);
65
+ if (opts.context !== false) {
66
+ payload.licenseEmail = await readLicenseEmail();
67
+ payload.recentCommands = await readRecentCommands();
68
+ }
69
+ return payload;
70
+ }
71
+ async function readCliVersion() {
72
+ try {
73
+ const pkgPath = path.join(
74
+ path.dirname(new URL(import.meta.url).pathname),
75
+ "..",
76
+ "..",
77
+ "package.json"
78
+ );
79
+ const raw = await fs.readFile(pkgPath, "utf8");
80
+ const obj = JSON.parse(raw);
81
+ return obj.version ?? "unknown";
82
+ } catch {
83
+ return "unknown";
84
+ }
85
+ }
86
+ async function readLicenseEmail() {
87
+ try {
88
+ const licensePath = path.join(os.homedir(), ".sdt", "license");
89
+ const raw = await fs.readFile(licensePath, "utf8");
90
+ const parts = raw.trim().split(".");
91
+ if (parts.length < 2) return void 0;
92
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
93
+ if (typeof payload === "object" && payload !== null) {
94
+ const sub = payload.sub;
95
+ if (typeof sub === "string") return sub;
96
+ }
97
+ return void 0;
98
+ } catch {
99
+ return void 0;
100
+ }
101
+ }
102
+ async function readRecentCommands() {
103
+ try {
104
+ const logPath = path.join(os.homedir(), ".sdt", "recent.log");
105
+ const raw = await fs.readFile(logPath, "utf8");
106
+ return raw.split(/\r?\n/).filter(Boolean).slice(-5);
107
+ } catch {
108
+ return void 0;
109
+ }
110
+ }
111
+ function normalizeCategory(input) {
112
+ const s = String(input ?? "other").toLowerCase();
113
+ if (s === "bug" || s === "feature" || s === "question" || s === "docs") return s;
114
+ return "other";
115
+ }
116
+ async function sendOrQueue(payload, url) {
117
+ await drainOutbox(url);
118
+ const res = await fetch(url, {
119
+ method: "POST",
120
+ headers: { "content-type": "application/json" },
121
+ body: JSON.stringify(payload)
122
+ });
123
+ if (!res.ok) {
124
+ throw new Error(`HTTP ${res.status}`);
125
+ }
126
+ }
127
+ async function queueLocally(payload) {
128
+ await fs.mkdir(OUTBOX_DIR, { recursive: true });
129
+ const file = path.join(OUTBOX_DIR, `${Date.now()}-${randomTag()}.json`);
130
+ await fs.writeFile(file, JSON.stringify(payload, null, 2));
131
+ return file;
132
+ }
133
+ async function drainOutbox(url) {
134
+ try {
135
+ const entries = await fs.readdir(OUTBOX_DIR);
136
+ for (const entry of entries) {
137
+ if (!entry.endsWith(".json")) continue;
138
+ const file = path.join(OUTBOX_DIR, entry);
139
+ try {
140
+ const raw = await fs.readFile(file, "utf8");
141
+ const res = await fetch(url, {
142
+ method: "POST",
143
+ headers: { "content-type": "application/json" },
144
+ body: raw
145
+ });
146
+ if (res.ok) {
147
+ await fs.unlink(file);
148
+ }
149
+ } catch {
150
+ }
151
+ }
152
+ } catch {
153
+ }
154
+ }
155
+ function randomTag() {
156
+ return Math.random().toString(36).slice(2, 8);
157
+ }
158
+ export {
159
+ feedbackCommand
160
+ };
161
+ //# sourceMappingURL=feedback-M7DM2EQC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/feedback.ts"],"sourcesContent":["/**\n * `sdt feedback \"...\"` — submit a feedback / bug / feature-request\n * ticket from inside the tool.\n *\n * Lowest-friction reporting surface. From any terminal:\n *\n * $ sdt feedback \"publish --apply takes 45s before doing anything\"\n * Sent! Reference: #847.\n *\n * Auto-captures (with user consent on first run):\n * - CLI version\n * - OS + platform\n * - License email (if a license is installed)\n * - Last 5 sdt invocations from ~/.sdt/recent.log (if it exists)\n *\n * Writes to a local outbox first (~/.sdt/feedback/<timestamp>.json)\n * so failures don't lose the report. Drains the outbox in the\n * background. Endpoint is configurable; the default is the\n * project's ingest URL.\n *\n * See `docs/SUPPORT_OPERATIONS.md` for the ingest pipeline.\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport os from 'node:os';\nimport { Command } from 'commander';\nimport { logger } from '../util/logger.js';\nimport { license } from '@sdt-tools/core';\n\nconst DEFAULT_INGEST_URL =\n process.env.SDT_FEEDBACK_URL ?? 'https://sdt-support.sdt-ddt-tools.workers.dev/ingest';\nconst OUTBOX_DIR = path.join(os.homedir(), '.sdt', 'feedback');\n\ninterface FeedbackPayload {\n body: string;\n product: 'sdt' | 'ddt';\n productVersion: string;\n os: string;\n nodeVersion: string;\n ts: string;\n contact?: string;\n licenseEmail?: string;\n recentCommands?: string[];\n category?: 'bug' | 'feature' | 'question' | 'docs' | 'other';\n}\n\nexport function feedbackCommand(): Command {\n const cmd = new Command('feedback');\n cmd\n .description('Send a feedback / bug / feature-request ticket to the SDT team.')\n .argument('<message...>', 'The feedback body (quote to keep as one arg).')\n .option('--category <kind>', 'Category: bug | feature | question | docs | other', 'other')\n .option(\n '--contact <email>',\n 'Reply-to email (optional; otherwise we use the license email if any).',\n )\n .option('--no-context', 'Skip auto-capture of CLI version / OS / recent commands.')\n .option('--dry-run', 'Print the payload without sending or queueing.')\n .option('--url <url>', `Override the ingest endpoint. Default ${DEFAULT_INGEST_URL}.`)\n .action(async (messageParts: string[], opts) => {\n const body = (messageParts ?? []).join(' ').trim();\n if (!body) {\n logger.error('Empty feedback body — nothing to send.');\n process.exitCode = 1;\n return;\n }\n const payload = await buildPayload(body, opts);\n if (opts.dryRun) {\n console.log(JSON.stringify(payload, null, 2));\n return;\n }\n try {\n await sendOrQueue(payload, String(opts.url ?? DEFAULT_INGEST_URL));\n logger.success('Feedback sent. Thank you!');\n logger.dim(' You should hear back within 1 business day for Pro+ accounts.');\n if (!payload.licenseEmail) {\n logger.dim(' Tip: install a license (`sdt license activate`) so we can reach you.');\n }\n const extended = await license.extendTrial(1);\n if (extended) {\n logger.dim(\n ` Feedback bonus: +1 trial day (now expires ${extended.expiresAt.slice(0, 10)}).`,\n );\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.warn(`Couldn't reach the ingest endpoint: ${message}`);\n const queued = await queueLocally(payload);\n logger.dim(` Queued locally at ${queued}. Will retry on next \\`sdt feedback\\` run.`);\n }\n });\n return cmd;\n}\n\nasync function buildPayload(\n body: string,\n opts: { category?: string; contact?: string; context?: boolean },\n): Promise<FeedbackPayload> {\n const category = normalizeCategory(opts.category);\n const payload: FeedbackPayload = {\n body,\n product: 'sdt',\n productVersion: await readCliVersion(),\n os: `${os.platform()} ${os.release()}`,\n nodeVersion: process.version,\n ts: new Date().toISOString(),\n category,\n };\n if (opts.contact) payload.contact = String(opts.contact);\n if (opts.context !== false) {\n payload.licenseEmail = await readLicenseEmail();\n payload.recentCommands = await readRecentCommands();\n }\n return payload;\n}\n\nasync function readCliVersion(): Promise<string> {\n try {\n const pkgPath = path.join(\n path.dirname(new URL(import.meta.url).pathname),\n '..',\n '..',\n 'package.json',\n );\n const raw = await fs.readFile(pkgPath, 'utf8');\n const obj = JSON.parse(raw) as { version?: string };\n return obj.version ?? 'unknown';\n } catch {\n return 'unknown';\n }\n}\n\nasync function readLicenseEmail(): Promise<string | undefined> {\n try {\n const licensePath = path.join(os.homedir(), '.sdt', 'license');\n const raw = await fs.readFile(licensePath, 'utf8');\n // License is an Ed25519 JWT — sub claim carries the email. Decode the\n // payload section without verifying (we just want the bound email for\n // display).\n const parts = raw.trim().split('.');\n if (parts.length < 2) return undefined;\n const payload: unknown = JSON.parse(Buffer.from(parts[1]!, 'base64url').toString('utf8'));\n if (typeof payload === 'object' && payload !== null) {\n const sub = (payload as { sub?: unknown }).sub;\n if (typeof sub === 'string') return sub;\n }\n return undefined;\n } catch {\n return undefined;\n }\n}\n\nasync function readRecentCommands(): Promise<string[] | undefined> {\n try {\n const logPath = path.join(os.homedir(), '.sdt', 'recent.log');\n const raw = await fs.readFile(logPath, 'utf8');\n return raw.split(/\\r?\\n/).filter(Boolean).slice(-5);\n } catch {\n return undefined;\n }\n}\n\nfunction normalizeCategory(input: unknown): FeedbackPayload['category'] {\n const s = String(input ?? 'other').toLowerCase();\n if (s === 'bug' || s === 'feature' || s === 'question' || s === 'docs') return s;\n return 'other';\n}\n\nasync function sendOrQueue(payload: FeedbackPayload, url: string): Promise<void> {\n // First: drain any previously-queued items (best effort).\n await drainOutbox(url);\n // Then send the new one.\n const res = await fetch(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n });\n if (!res.ok) {\n throw new Error(`HTTP ${res.status}`);\n }\n}\n\nasync function queueLocally(payload: FeedbackPayload): Promise<string> {\n await fs.mkdir(OUTBOX_DIR, { recursive: true });\n const file = path.join(OUTBOX_DIR, `${Date.now()}-${randomTag()}.json`);\n await fs.writeFile(file, JSON.stringify(payload, null, 2));\n return file;\n}\n\nasync function drainOutbox(url: string): Promise<void> {\n try {\n const entries = await fs.readdir(OUTBOX_DIR);\n for (const entry of entries) {\n if (!entry.endsWith('.json')) continue;\n const file = path.join(OUTBOX_DIR, entry);\n try {\n const raw = await fs.readFile(file, 'utf8');\n const res = await fetch(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: raw,\n });\n if (res.ok) {\n await fs.unlink(file);\n }\n } catch {\n // Keep in outbox; will retry next time.\n }\n }\n } catch {\n // No outbox; nothing to drain.\n }\n}\n\nfunction randomTag(): string {\n return Math.random().toString(36).slice(2, 8);\n}\n"],"mappings":";;;;;;AAsBA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,eAAe;AAExB,SAAS,eAAe;AAExB,IAAM,qBACJ,QAAQ,IAAI,oBAAoB;AAClC,IAAM,aAAa,KAAK,KAAK,GAAG,QAAQ,GAAG,QAAQ,UAAU;AAetD,SAAS,kBAA2B;AACzC,QAAM,MAAM,IAAI,QAAQ,UAAU;AAClC,MACG,YAAY,iEAAiE,EAC7E,SAAS,gBAAgB,+CAA+C,EACxE,OAAO,qBAAqB,qDAAqD,OAAO,EACxF;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC,OAAO,gBAAgB,0DAA0D,EACjF,OAAO,aAAa,gDAAgD,EACpE,OAAO,eAAe,yCAAyC,kBAAkB,GAAG,EACpF,OAAO,OAAO,cAAwB,SAAS;AAC9C,UAAM,QAAQ,gBAAgB,CAAC,GAAG,KAAK,GAAG,EAAE,KAAK;AACjD,QAAI,CAAC,MAAM;AACT,aAAO,MAAM,6CAAwC;AACrD,cAAQ,WAAW;AACnB;AAAA,IACF;AACA,UAAM,UAAU,MAAM,aAAa,MAAM,IAAI;AAC7C,QAAI,KAAK,QAAQ;AACf,cAAQ,IAAI,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AAC5C;AAAA,IACF;AACA,QAAI;AACF,YAAM,YAAY,SAAS,OAAO,KAAK,OAAO,kBAAkB,CAAC;AACjE,aAAO,QAAQ,2BAA2B;AAC1C,aAAO,IAAI,iEAAiE;AAC5E,UAAI,CAAC,QAAQ,cAAc;AACzB,eAAO,IAAI,wEAAwE;AAAA,MACrF;AACA,YAAM,WAAW,MAAM,QAAQ,YAAY,CAAC;AAC5C,UAAI,UAAU;AACZ,eAAO;AAAA,UACL,+CAA+C,SAAS,UAAU,MAAM,GAAG,EAAE,CAAC;AAAA,QAChF;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,YAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,aAAO,KAAK,uCAAuC,OAAO,EAAE;AAC5D,YAAM,SAAS,MAAM,aAAa,OAAO;AACzC,aAAO,IAAI,uBAAuB,MAAM,4CAA4C;AAAA,IACtF;AAAA,EACF,CAAC;AACH,SAAO;AACT;AAEA,eAAe,aACb,MACA,MAC0B;AAC1B,QAAM,WAAW,kBAAkB,KAAK,QAAQ;AAChD,QAAM,UAA2B;AAAA,IAC/B;AAAA,IACA,SAAS;AAAA,IACT,gBAAgB,MAAM,eAAe;AAAA,IACrC,IAAI,GAAG,GAAG,SAAS,CAAC,IAAI,GAAG,QAAQ,CAAC;AAAA,IACpC,aAAa,QAAQ;AAAA,IACrB,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,IAC3B;AAAA,EACF;AACA,MAAI,KAAK,QAAS,SAAQ,UAAU,OAAO,KAAK,OAAO;AACvD,MAAI,KAAK,YAAY,OAAO;AAC1B,YAAQ,eAAe,MAAM,iBAAiB;AAC9C,YAAQ,iBAAiB,MAAM,mBAAmB;AAAA,EACpD;AACA,SAAO;AACT;AAEA,eAAe,iBAAkC;AAC/C,MAAI;AACF,UAAM,UAAU,KAAK;AAAA,MACnB,KAAK,QAAQ,IAAI,IAAI,YAAY,GAAG,EAAE,QAAQ;AAAA,MAC9C;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,UAAM,MAAM,MAAM,GAAG,SAAS,SAAS,MAAM;AAC7C,UAAM,MAAM,KAAK,MAAM,GAAG;AAC1B,WAAO,IAAI,WAAW;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,mBAAgD;AAC7D,MAAI;AACF,UAAM,cAAc,KAAK,KAAK,GAAG,QAAQ,GAAG,QAAQ,SAAS;AAC7D,UAAM,MAAM,MAAM,GAAG,SAAS,aAAa,MAAM;AAIjD,UAAM,QAAQ,IAAI,KAAK,EAAE,MAAM,GAAG;AAClC,QAAI,MAAM,SAAS,EAAG,QAAO;AAC7B,UAAM,UAAmB,KAAK,MAAM,OAAO,KAAK,MAAM,CAAC,GAAI,WAAW,EAAE,SAAS,MAAM,CAAC;AACxF,QAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,YAAM,MAAO,QAA8B;AAC3C,UAAI,OAAO,QAAQ,SAAU,QAAO;AAAA,IACtC;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,qBAAoD;AACjE,MAAI;AACF,UAAM,UAAU,KAAK,KAAK,GAAG,QAAQ,GAAG,QAAQ,YAAY;AAC5D,UAAM,MAAM,MAAM,GAAG,SAAS,SAAS,MAAM;AAC7C,WAAO,IAAI,MAAM,OAAO,EAAE,OAAO,OAAO,EAAE,MAAM,EAAE;AAAA,EACpD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,kBAAkB,OAA6C;AACtE,QAAM,IAAI,OAAO,SAAS,OAAO,EAAE,YAAY;AAC/C,MAAI,MAAM,SAAS,MAAM,aAAa,MAAM,cAAc,MAAM,OAAQ,QAAO;AAC/E,SAAO;AACT;AAEA,eAAe,YAAY,SAA0B,KAA4B;AAE/E,QAAM,YAAY,GAAG;AAErB,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU,OAAO;AAAA,EAC9B,CAAC;AACD,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,IAAI,MAAM,QAAQ,IAAI,MAAM,EAAE;AAAA,EACtC;AACF;AAEA,eAAe,aAAa,SAA2C;AACrE,QAAM,GAAG,MAAM,YAAY,EAAE,WAAW,KAAK,CAAC;AAC9C,QAAM,OAAO,KAAK,KAAK,YAAY,GAAG,KAAK,IAAI,CAAC,IAAI,UAAU,CAAC,OAAO;AACtE,QAAM,GAAG,UAAU,MAAM,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AACzD,SAAO;AACT;AAEA,eAAe,YAAY,KAA4B;AACrD,MAAI;AACF,UAAM,UAAU,MAAM,GAAG,QAAQ,UAAU;AAC3C,eAAW,SAAS,SAAS;AAC3B,UAAI,CAAC,MAAM,SAAS,OAAO,EAAG;AAC9B,YAAM,OAAO,KAAK,KAAK,YAAY,KAAK;AACxC,UAAI;AACF,cAAM,MAAM,MAAM,GAAG,SAAS,MAAM,MAAM;AAC1C,cAAM,MAAM,MAAM,MAAM,KAAK;AAAA,UAC3B,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM;AAAA,QACR,CAAC;AACD,YAAI,IAAI,IAAI;AACV,gBAAM,GAAG,OAAO,IAAI;AAAA,QACtB;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,YAAoB;AAC3B,SAAO,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC;AAC9C;","names":[]}
@@ -0,0 +1,176 @@
1
+ import "./chunk-DGUM43GV.js";
2
+
3
+ // src/commands/find.ts
4
+ import { promises as fs } from "fs";
5
+ import path from "path";
6
+ import { homedir } from "os";
7
+ import { Command } from "commander";
8
+ import { features as featuresApi, license as licenseApi } from "@sdt-tools/core";
9
+ import { complete } from "@sdt-tools/core/ai";
10
+ import chalk from "chalk";
11
+ function tierBadge(tier) {
12
+ switch (tier) {
13
+ case "free":
14
+ return chalk.green("Free");
15
+ case "pro":
16
+ return chalk.cyan("Pro");
17
+ case "team":
18
+ return chalk.yellow("Team");
19
+ case "enterprise":
20
+ return chalk.magenta("Enterprise");
21
+ default:
22
+ return tier;
23
+ }
24
+ }
25
+ function findCommand() {
26
+ const cmd = new Command("find");
27
+ cmd.description("Search SDT features by keyword \u2014 find what you want to do.").argument("<query...>", 'Keywords to search for (e.g. "drift detection", "multi-team").').option("-n, --limit <n>", "Max results to show.", "5").option("--format <fmt>", "Output format: text | json.", "text").option("--tier <tier>", "Filter by tier: free | pro | team | enterprise.").option("--shipped", "Only include shipped features (exclude roadmap).").option(
28
+ "--ai",
29
+ "DSC.4: escalate to the configured AI provider when deterministic search yields low-confidence hits. Costs tokens; default off."
30
+ ).option(
31
+ "--ai-threshold <n>",
32
+ "Confidence below which --ai triggers escalation (0..1, default 0.55).",
33
+ "0.55"
34
+ ).option(
35
+ "--no-ai-cache",
36
+ "DSC.4-followup: skip the on-disk intent cache and always re-query the AI provider when --ai escalates."
37
+ ).action(async (queryParts, opts) => {
38
+ const query = queryParts.join(" ").trim();
39
+ const limit = Math.max(1, parseInt(String(opts.limit), 10) || 5);
40
+ const fmt = String(opts.format ?? "text").toLowerCase();
41
+ const tierFilter = opts.tier;
42
+ const statusFilter = opts.shipped ? "shipped" : void 0;
43
+ const licenseObj = await licenseApi.loadLicense();
44
+ let hits = [];
45
+ let stage = 1;
46
+ let tokensUsed = 0;
47
+ let cacheHit = false;
48
+ if (opts.ai) {
49
+ const threshold = Math.max(0, Math.min(1, Number(opts.aiThreshold ?? "0.55")));
50
+ const cachePath = path.join(homedir(), ".sdt", "intent-cache.json");
51
+ const useCache = opts.aiCache !== false;
52
+ let cache = featuresApi.emptyIntentCache();
53
+ if (useCache) {
54
+ try {
55
+ const body = await fs.readFile(cachePath, "utf8");
56
+ cache = featuresApi.parseIntentCache(body);
57
+ } catch {
58
+ }
59
+ const cached = featuresApi.lookupIntent(cache, query);
60
+ if (cached) {
61
+ const idSet = new Set(cached.featureIds);
62
+ const matchedFeatures = featuresApi.SDT_FEATURE_CATALOG.filter(
63
+ (f) => idSet.has(f.id)
64
+ ).sort((a, b) => cached.featureIds.indexOf(a.id) - cached.featureIds.indexOf(b.id));
65
+ hits = matchedFeatures.slice(0, limit).map((f) => ({
66
+ feature: f,
67
+ score: 1,
68
+ matchedFields: ["ai-intent-cached"]
69
+ }));
70
+ stage = 2;
71
+ tokensUsed = 0;
72
+ cacheHit = true;
73
+ }
74
+ }
75
+ if (!cacheHit) {
76
+ const result = await featuresApi.intentMatch(
77
+ query,
78
+ featuresApi.SDT_FEATURE_CATALOG,
79
+ { limit, tier: tierFilter, status: statusFilter, threshold },
80
+ (messages) => complete(messages, { feature: "find.intent" })
81
+ );
82
+ hits = [...result.hits];
83
+ stage = result.stage;
84
+ tokensUsed = result.tokensUsed;
85
+ if (useCache && stage === 2 && hits.length > 0) {
86
+ const next = featuresApi.evictExpiredIntents(
87
+ featuresApi.addIntent(
88
+ cache,
89
+ query,
90
+ hits.map((h) => h.feature.id),
91
+ tokensUsed
92
+ )
93
+ );
94
+ try {
95
+ await fs.mkdir(path.dirname(cachePath), { recursive: true });
96
+ await fs.writeFile(cachePath, featuresApi.serializeIntentCache(next), "utf8");
97
+ } catch {
98
+ }
99
+ }
100
+ }
101
+ } else {
102
+ hits = featuresApi.searchFeatures(query, featuresApi.SDT_FEATURE_CATALOG, {
103
+ limit,
104
+ tier: tierFilter,
105
+ status: statusFilter
106
+ });
107
+ }
108
+ if (fmt === "json") {
109
+ process.stdout.write(
110
+ JSON.stringify(
111
+ hits.map((h) => ({
112
+ id: h.feature.id,
113
+ name: h.feature.name,
114
+ tier: h.feature.tier,
115
+ status: h.feature.status,
116
+ score: h.score,
117
+ matchedFields: h.matchedFields,
118
+ summary: h.feature.summary,
119
+ unlockHow: h.feature.unlockHow,
120
+ useCases: h.feature.useCases,
121
+ relatedFeatures: h.feature.relatedFeatures
122
+ })),
123
+ null,
124
+ 2
125
+ ) + "\n"
126
+ );
127
+ return;
128
+ }
129
+ if (hits.length === 0) {
130
+ process.stdout.write(
131
+ chalk.dim(` No features found for "${query}".
132
+ `) + chalk.dim(` Try: sdt features list \xB7 sdt explain <topic>
133
+ `)
134
+ );
135
+ return;
136
+ }
137
+ process.stdout.write("\n");
138
+ for (const hit of hits) {
139
+ const avail = featuresApi.isFeatureAvailable(hit.feature, licenseObj);
140
+ const statusMark = hit.feature.status === "roadmap" ? chalk.dim("\u{1F6A7} roadmap") : avail ? chalk.green("\u2705 available") : chalk.dim("\u{1F512} locked");
141
+ process.stdout.write(
142
+ ` ${chalk.bold(hit.feature.name)} ${tierBadge(hit.feature.tier)} ${statusMark}
143
+ ${chalk.dim(hit.feature.summary)}
144
+ `
145
+ );
146
+ if (hit.feature.useCases && hit.feature.useCases.length > 0) {
147
+ process.stdout.write(chalk.dim(` Use when: ${hit.feature.useCases[0]}
148
+ `));
149
+ }
150
+ if (!avail && hit.feature.status === "shipped") {
151
+ process.stdout.write(chalk.dim(` Unlock: ${hit.feature.unlockHow}
152
+ `));
153
+ }
154
+ if (hit.feature.surfaces && hit.feature.surfaces.length > 0) {
155
+ process.stdout.write(
156
+ chalk.dim(` Surface: ${hit.feature.surfaces.slice(0, 2).join(", ")}
157
+ `)
158
+ );
159
+ }
160
+ process.stdout.write(chalk.dim(` sdt features show ${hit.feature.id}
161
+
162
+ `));
163
+ }
164
+ const aiNote = stage === 2 ? cacheHit ? " (AI-cached, 0 tokens)" : ` (AI-escalated, ${tokensUsed} tokens)` : "";
165
+ process.stdout.write(
166
+ chalk.dim(
167
+ ` ${hits.length} result${hits.length === 1 ? "" : "s"} for "${query}"${aiNote}. `
168
+ ) + chalk.dim("sdt features list \xB7 sdt features show <id>\n")
169
+ );
170
+ });
171
+ return cmd;
172
+ }
173
+ export {
174
+ findCommand
175
+ };
176
+ //# sourceMappingURL=find-EME2JG2I.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/find.ts"],"sourcesContent":["/**\n * `sdt find <query>` — search the SDT feature catalog by keyword.\n *\n * DSC.1: surface any SDT capability by describing what you want to do.\n *\n * $ sdt find drift\n * $ sdt find \"multi-team database\"\n * $ sdt find compare --limit 3 --format json\n *\n * Reads the same catalog as `sdt features list`. Scoring favors exact\n * id/name/synonym matches over summary body matches. Results include\n * the unlock path so Free users know exactly what each feature needs.\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { homedir } from 'node:os';\nimport { Command } from 'commander';\nimport { features as featuresApi, license as licenseApi } from '@sdt-tools/core';\nimport { complete } from '@sdt-tools/core/ai';\nimport chalk from 'chalk';\n\nfunction tierBadge(tier: string): string {\n switch (tier) {\n case 'free':\n return chalk.green('Free');\n case 'pro':\n return chalk.cyan('Pro');\n case 'team':\n return chalk.yellow('Team');\n case 'enterprise':\n return chalk.magenta('Enterprise');\n default:\n return tier;\n }\n}\n\nexport function findCommand(): Command {\n const cmd = new Command('find');\n cmd\n .description('Search SDT features by keyword — find what you want to do.')\n .argument('<query...>', 'Keywords to search for (e.g. \"drift detection\", \"multi-team\").')\n .option('-n, --limit <n>', 'Max results to show.', '5')\n .option('--format <fmt>', 'Output format: text | json.', 'text')\n .option('--tier <tier>', 'Filter by tier: free | pro | team | enterprise.')\n .option('--shipped', 'Only include shipped features (exclude roadmap).')\n .option(\n '--ai',\n 'DSC.4: escalate to the configured AI provider when deterministic search yields low-confidence hits. Costs tokens; default off.',\n )\n .option(\n '--ai-threshold <n>',\n 'Confidence below which --ai triggers escalation (0..1, default 0.55).',\n '0.55',\n )\n .option(\n '--no-ai-cache',\n 'DSC.4-followup: skip the on-disk intent cache and always re-query the AI provider when --ai escalates.',\n )\n .action(async (queryParts: string[], opts) => {\n const query = queryParts.join(' ').trim();\n const limit = Math.max(1, parseInt(String(opts.limit), 10) || 5);\n const fmt = String(opts.format ?? 'text').toLowerCase();\n const tierFilter = opts.tier as 'free' | 'pro' | 'team' | 'enterprise' | undefined;\n const statusFilter = opts.shipped ? ('shipped' as const) : undefined;\n\n const licenseObj = await licenseApi.loadLicense();\n let hits: ReturnType<typeof featuresApi.searchFeatures> = [];\n let stage: 1 | 2 = 1;\n let tokensUsed = 0;\n let cacheHit = false;\n if (opts.ai) {\n const threshold = Math.max(0, Math.min(1, Number(opts.aiThreshold ?? '0.55')));\n // DSC.4-followup — 24h-TTL disk cache at ~/.sdt/intent-cache.json.\n // Skipped entirely when --no-ai-cache is passed; otherwise:\n // load → check → maybe call → evict stale → save.\n const cachePath = path.join(homedir(), '.sdt', 'intent-cache.json');\n const useCache = opts.aiCache !== false;\n let cache = featuresApi.emptyIntentCache();\n if (useCache) {\n try {\n const body = await fs.readFile(cachePath, 'utf8');\n cache = featuresApi.parseIntentCache(body);\n } catch {\n // No cache yet — start fresh.\n }\n const cached = featuresApi.lookupIntent(cache, query);\n if (cached) {\n const idSet = new Set(cached.featureIds);\n const matchedFeatures = featuresApi.SDT_FEATURE_CATALOG.filter((f) =>\n idSet.has(f.id),\n ).sort((a, b) => cached.featureIds.indexOf(a.id) - cached.featureIds.indexOf(b.id));\n hits = matchedFeatures.slice(0, limit).map((f) => ({\n feature: f,\n score: 1.0,\n matchedFields: ['ai-intent-cached'],\n }));\n stage = 2;\n tokensUsed = 0;\n cacheHit = true;\n }\n }\n if (!cacheHit) {\n const result = await featuresApi.intentMatch(\n query,\n featuresApi.SDT_FEATURE_CATALOG,\n { limit, tier: tierFilter, status: statusFilter, threshold },\n (messages) => complete(messages, { feature: 'find.intent' }),\n );\n hits = [...result.hits];\n stage = result.stage;\n tokensUsed = result.tokensUsed;\n // Cache only AI-escalated results — Stage 1 deterministic hits\n // are already fast and don't need a cache round-trip.\n if (useCache && stage === 2 && hits.length > 0) {\n const next = featuresApi.evictExpiredIntents(\n featuresApi.addIntent(\n cache,\n query,\n hits.map((h) => h.feature.id),\n tokensUsed,\n ),\n );\n try {\n await fs.mkdir(path.dirname(cachePath), { recursive: true });\n await fs.writeFile(cachePath, featuresApi.serializeIntentCache(next), 'utf8');\n } catch {\n // Best-effort — don't fail the command if disk is full.\n }\n }\n }\n } else {\n hits = featuresApi.searchFeatures(query, featuresApi.SDT_FEATURE_CATALOG, {\n limit,\n tier: tierFilter,\n status: statusFilter,\n });\n }\n\n if (fmt === 'json') {\n process.stdout.write(\n JSON.stringify(\n hits.map((h) => ({\n id: h.feature.id,\n name: h.feature.name,\n tier: h.feature.tier,\n status: h.feature.status,\n score: h.score,\n matchedFields: h.matchedFields,\n summary: h.feature.summary,\n unlockHow: h.feature.unlockHow,\n useCases: h.feature.useCases,\n relatedFeatures: h.feature.relatedFeatures,\n })),\n null,\n 2,\n ) + '\\n',\n );\n return;\n }\n\n if (hits.length === 0) {\n process.stdout.write(\n chalk.dim(` No features found for \"${query}\".\\n`) +\n chalk.dim(` Try: sdt features list · sdt explain <topic>\\n`),\n );\n return;\n }\n\n process.stdout.write('\\n');\n for (const hit of hits) {\n const avail = featuresApi.isFeatureAvailable(hit.feature, licenseObj);\n const statusMark =\n hit.feature.status === 'roadmap'\n ? chalk.dim('🚧 roadmap')\n : avail\n ? chalk.green('✅ available')\n : chalk.dim('🔒 locked');\n\n process.stdout.write(\n ` ${chalk.bold(hit.feature.name)} ${tierBadge(hit.feature.tier)} ${statusMark}\\n` +\n ` ${chalk.dim(hit.feature.summary)}\\n`,\n );\n if (hit.feature.useCases && hit.feature.useCases.length > 0) {\n process.stdout.write(chalk.dim(` Use when: ${hit.feature.useCases[0]}\\n`));\n }\n if (!avail && hit.feature.status === 'shipped') {\n process.stdout.write(chalk.dim(` Unlock: ${hit.feature.unlockHow}\\n`));\n }\n if (hit.feature.surfaces && hit.feature.surfaces.length > 0) {\n process.stdout.write(\n chalk.dim(` Surface: ${hit.feature.surfaces.slice(0, 2).join(', ')}\\n`),\n );\n }\n process.stdout.write(chalk.dim(` sdt features show ${hit.feature.id}\\n\\n`));\n }\n\n const aiNote =\n stage === 2\n ? cacheHit\n ? ' (AI-cached, 0 tokens)'\n : ` (AI-escalated, ${tokensUsed} tokens)`\n : '';\n process.stdout.write(\n chalk.dim(\n ` ${hits.length} result${hits.length === 1 ? '' : 's'} for \"${query}\"${aiNote}. `,\n ) + chalk.dim('sdt features list · sdt features show <id>\\n'),\n );\n });\n return cmd;\n}\n"],"mappings":"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","names":[]}