@openpolicy/cli 0.0.10 → 0.0.12

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 (3) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +153 -294
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -41,6 +41,7 @@ openpolicy generate ./terms.config.ts --format markdown --out ./public/po
41
41
  | `--format` | `markdown` | Comma-separated output formats: `markdown`, `html` |
42
42
  | `--out` | `./public/policies` | Output directory |
43
43
  | `--type` | auto-detected | Override policy type: `privacy` or `terms` |
44
+ | `--watch` | `false` | Watch config files and regenerate on changes |
44
45
 
45
46
  Policy type is auto-detected from the filename — files containing `"terms"` compile as terms of service.
46
47
 
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { defineCommand, runMain } from "citty";
3
+ import { mkdir, writeFile } from "node:fs/promises";
3
4
  import { join, resolve } from "node:path";
4
5
  import consola from "consola";
5
- import { access, mkdir, writeFile } from "node:fs/promises";
6
- import { compilePolicy, validatePrivacyPolicy, validateTermsOfService } from "@openpolicy/core";
7
- import { existsSync } from "node:fs";
6
+ import { existsSync, watch } from "node:fs";
7
+ import { compilePolicy, expandOpenPolicyConfig, isOpenPolicyConfig, validateCookiePolicy, validatePrivacyPolicy, validateTermsOfService } from "@openpolicy/core";
8
8
  //#region \0rolldown/runtime.js
9
9
  var __defProp = Object.defineProperty;
10
10
  var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -18,270 +18,121 @@ var __exportAll = (all, no_symbols) => {
18
18
  return target;
19
19
  };
20
20
  //#endregion
21
+ //#region package.json
22
+ var version = "0.0.12";
23
+ //#endregion
21
24
  //#region src/commands/init.ts
22
- var init_exports = /* @__PURE__ */ __exportAll({ initCommand: () => initCommand });
23
- function toJurisdictions(choice) {
24
- if (choice === "gdpr") return ["eu"];
25
- if (choice === "ccpa") return ["ca"];
26
- if (choice === "both") return ["eu", "ca"];
27
- return ["us"];
28
- }
29
- function toDataCollected(categories) {
30
- const groups = {};
31
- for (const cat of categories) {
32
- const mapping = DATA_CATEGORY_MAP[cat];
33
- if (!mapping) continue;
34
- groups[mapping.group] = [...groups[mapping.group] ?? [], mapping.label];
35
- }
36
- return Object.keys(groups).length > 0 ? groups : { "Personal Information": ["Email address"] };
37
- }
38
- function toUserRights(jurisdictions) {
39
- const rights = new Set(["access", "erasure"]);
40
- if (jurisdictions.includes("eu")) for (const r of [
41
- "rectification",
42
- "portability",
43
- "restriction",
44
- "objection"
45
- ]) rights.add(r);
46
- if (jurisdictions.includes("ca")) for (const r of ["opt_out_sale", "non_discrimination"]) rights.add(r);
47
- return Array.from(rights);
48
- }
49
- function renderPrivacyConfig(values) {
50
- const dataLines = Object.entries(values.dataCollected).map(([k, v]) => ` ${JSON.stringify(k)}: ${JSON.stringify(v)},`).join("\n");
51
- return `import { definePrivacyPolicy } from "@openpolicy/sdk";
52
-
53
- export default definePrivacyPolicy({
54
- effectiveDate: "${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}",
55
- company: {
56
- name: ${JSON.stringify(values.companyName)},
57
- legalName: ${JSON.stringify(values.legalName)},
58
- address: ${JSON.stringify(values.address)},
59
- contact: ${JSON.stringify(values.contact)},
60
- },
61
- dataCollected: {
62
- ${dataLines}
63
- },
64
- legalBasis: ${JSON.stringify(values.legalBasis)},
65
- retention: {
66
- "All personal data": "As long as necessary for the purposes described in this policy",
67
- },
68
- cookies: {
69
- essential: true,
70
- analytics: ${values.hasCookies},
71
- marketing: false,
72
- },
73
- thirdParties: [],
74
- userRights: ${JSON.stringify(values.userRights)},
75
- jurisdictions: ${JSON.stringify(values.jurisdictions)},
25
+ var init_exports = /* @__PURE__ */ __exportAll({
26
+ getOpenPolicyTemplate: () => getOpenPolicyTemplate,
27
+ initCommand: () => initCommand
76
28
  });
77
- `;
78
- }
79
- function renderTermsConfig(values) {
80
- return `import { defineTermsOfService } from "@openpolicy/sdk";
29
+ function getOpenPolicyTemplate(companyName, contactEmail, policies) {
30
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
31
+ return `import { defineConfig } from "@openpolicy/sdk";
81
32
 
82
- export default defineTermsOfService({
83
- effectiveDate: "${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}",
84
- company: {
85
- name: ${JSON.stringify(values.companyName)},
86
- legalName: ${JSON.stringify(values.legalName)},
87
- address: ${JSON.stringify(values.address)},
88
- contact: ${JSON.stringify(values.contact)},
89
- },
90
- acceptance: {
91
- methods: ["using the service", "creating an account"],
92
- },
93
- eligibility: {
94
- minimumAge: 13,
95
- },
96
- accounts: {
97
- registrationRequired: false,
98
- userResponsibleForCredentials: true,
99
- companyCanTerminate: true,
100
- },
101
- prohibitedUses: [
102
- "Violating any applicable laws or regulations",
103
- "Infringing on intellectual property rights",
104
- "Transmitting harmful or malicious content",
105
- ],
106
- intellectualProperty: {
107
- companyOwnsService: true,
108
- usersMayNotCopy: true,
109
- },
110
- termination: {
111
- companyCanTerminate: true,
112
- userCanTerminate: true,
113
- },
114
- disclaimers: {
115
- serviceProvidedAsIs: true,
116
- noWarranties: true,
117
- },
118
- limitationOfLiability: {
119
- excludesIndirectDamages: true,
120
- },
121
- governingLaw: {
122
- jurisdiction: ${JSON.stringify(values.jurisdiction)},
123
- },
124
- changesPolicy: {
125
- noticeMethod: "email or prominent notice on our website",
126
- noticePeriodDays: 30,
127
- },
128
- });
129
- `;
130
- }
131
- var DATA_CATEGORY_MAP, initCommand;
132
- var init_init = __esmMin((() => {
133
- DATA_CATEGORY_MAP = {
134
- name: {
135
- group: "Personal Information",
136
- label: "Full name"
33
+ export default defineConfig({
34
+ company: {
35
+ name: "${companyName}",
36
+ legalName: "${companyName}",
37
+ address: "",
38
+ contact: "${contactEmail}",
39
+ },${policies.includes("privacy") ? `
40
+ privacy: {
41
+ effectiveDate: "${today}",
42
+ dataCollected: {
43
+ "Personal Information": ["Email address"],
137
44
  },
138
- email: {
139
- group: "Personal Information",
140
- label: "Email address"
45
+ legalBasis: "Legitimate interests",
46
+ retention: {
47
+ "All personal data": "As long as necessary for the purposes described in this policy",
141
48
  },
142
- ip_address: {
143
- group: "Technical Data",
144
- label: "IP address"
49
+ cookies: { essential: true, analytics: false, marketing: false },
50
+ thirdParties: [],
51
+ userRights: ["access", "erasure"],
52
+ jurisdictions: ["us"],
53
+ },` : ""}${policies.includes("terms") ? `
54
+ terms: {
55
+ effectiveDate: "${today}",
56
+ acceptance: { methods: ["using the service", "creating an account"] },
57
+ eligibility: { minimumAge: 13 },
58
+ accounts: {
59
+ registrationRequired: false,
60
+ userResponsibleForCredentials: true,
61
+ companyCanTerminate: true,
145
62
  },
146
- device_info: {
147
- group: "Technical Data",
148
- label: "Device type and browser"
63
+ prohibitedUses: [
64
+ "Violating any applicable laws or regulations",
65
+ "Infringing on intellectual property rights",
66
+ "Transmitting harmful or malicious content",
67
+ ],
68
+ intellectualProperty: { companyOwnsService: true, usersMayNotCopy: true },
69
+ termination: { companyCanTerminate: true, userCanTerminate: true },
70
+ disclaimers: { serviceProvidedAsIs: true, noWarranties: true },
71
+ limitationOfLiability: { excludesIndirectDamages: true },
72
+ governingLaw: { jurisdiction: "Delaware, USA" },
73
+ changesPolicy: {
74
+ noticeMethod: "email or prominent notice on our website",
75
+ noticePeriodDays: 30,
149
76
  },
150
- location: {
151
- group: "Location Data",
152
- label: "Approximate location"
153
- },
154
- payment_info: {
155
- group: "Financial Data",
156
- label: "Payment card details"
157
- },
158
- usage_data: {
159
- group: "Usage Data",
160
- label: "Pages visited and features used"
161
- }
162
- };
77
+ },` : ""}${policies.includes("cookie") ? `
78
+ cookie: {
79
+ effectiveDate: "${today}",
80
+ cookies: { essential: true, analytics: false, functional: false, marketing: false },
81
+ jurisdictions: ["us"],
82
+ },` : ""}
83
+ });
84
+ `;
85
+ }
86
+ var initCommand;
87
+ var init_init = __esmMin((() => {
163
88
  initCommand = defineCommand({
164
89
  meta: {
165
90
  name: "init",
166
91
  description: "Interactively create a policy config file"
167
92
  },
168
- args: {
169
- out: {
170
- type: "string",
171
- description: "Output path for generated config",
172
- default: ""
173
- },
174
- yes: {
175
- type: "boolean",
176
- description: "Skip prompts and use defaults (CI mode)",
177
- default: false
178
- },
179
- type: {
180
- type: "string",
181
- description: "Policy type: \"privacy\" or \"terms\"",
182
- default: "privacy"
183
- }
184
- },
93
+ args: { out: {
94
+ type: "string",
95
+ description: "Output path for generated config",
96
+ default: "openpolicy.ts"
97
+ } },
185
98
  async run({ args }) {
186
- const policyType = args.type === "terms" ? "terms" : "privacy";
187
- const defaultOut = policyType === "terms" ? "./terms.config.ts" : "./privacy.config.ts";
188
- consola.start(`OpenPolicy init wizard (${policyType})`);
189
- const companyName = String(await consola.prompt("Company name?", {
190
- type: "text",
191
- cancel: "reject"
192
- }));
193
- const legalName = String(await consola.prompt("Legal entity name?", {
194
- type: "text",
195
- cancel: "reject",
196
- initial: companyName
197
- }));
198
- const address = String(await consola.prompt("Company address?", {
99
+ consola.box("Welcome to OpenPolicy\nGenerate privacy policies, terms of service, and cookie policies from a single config file.");
100
+ consola.start("Let's get you set up.");
101
+ const source = getOpenPolicyTemplate(String(await consola.prompt("Company name?", {
199
102
  type: "text",
200
103
  cancel: "reject"
201
- }));
202
- const contact = String(await consola.prompt(policyType === "terms" ? "Legal contact email?" : "Privacy contact email?", {
104
+ })), String(await consola.prompt("Contact email?", {
203
105
  type: "text",
204
106
  cancel: "reject"
107
+ })), await consola.prompt("Which policies do you need?", {
108
+ type: "multiselect",
109
+ cancel: "reject",
110
+ options: [
111
+ "privacy",
112
+ "terms",
113
+ "cookie"
114
+ ]
205
115
  }));
206
- let source;
207
- if (policyType === "terms") source = renderTermsConfig({
208
- companyName,
209
- legalName,
210
- address,
211
- contact,
212
- jurisdiction: String(await consola.prompt("Governing law jurisdiction? (e.g. Delaware, USA)", {
213
- type: "text",
214
- cancel: "reject",
215
- initial: "Delaware, USA"
216
- }))
217
- });
218
- else {
219
- const jurisdictionChoice = String(await consola.prompt("Jurisdiction?", {
220
- type: "select",
221
- cancel: "reject",
222
- options: [
223
- "gdpr",
224
- "ccpa",
225
- "both"
226
- ]
227
- }));
228
- const dataCategories = await consola.prompt("Data categories collected?", {
229
- type: "multiselect",
230
- cancel: "reject",
231
- options: [
232
- "name",
233
- "email",
234
- "ip_address",
235
- "device_info",
236
- "location",
237
- "payment_info",
238
- "usage_data"
239
- ]
240
- });
241
- const hasCookies = Boolean(await consola.prompt("Does your app use cookies?", {
242
- type: "confirm",
243
- cancel: "reject",
244
- initial: true
245
- }));
246
- const jurisdictions = toJurisdictions(jurisdictionChoice);
247
- const dataCollected = toDataCollected(dataCategories);
248
- const userRights = toUserRights(jurisdictions);
249
- source = renderPrivacyConfig({
250
- companyName,
251
- legalName,
252
- address,
253
- contact,
254
- jurisdictions,
255
- dataCollected,
256
- legalBasis: jurisdictions.includes("eu") ? "Legitimate interests and consent" : "",
257
- hasCookies,
258
- userRights
259
- });
260
- }
261
- const outPath = resolve(args.out || defaultOut);
262
- await Bun.write(outPath, source);
116
+ const outPath = resolve(args.out);
117
+ await writeFile(outPath, source);
263
118
  consola.success(`Config written to ${outPath}`);
119
+ consola.info(`Open ${outPath} and fill in your company's details — address, legal name, and any policy-specific fields.`);
120
+ consola.info(`When you're ready, run:\n\n openpolicy generate ${args.out}\n\nto compile your policies to HTML or Markdown.`);
264
121
  }
265
122
  });
266
123
  }));
267
124
  //#endregion
268
- //#region src/utils/detect-type.ts
269
- function detectType(explicitType, configPath) {
270
- if (explicitType === "privacy" || explicitType === "terms") return explicitType;
271
- return configPath.toLowerCase().includes("terms") ? "terms" : "privacy";
272
- }
273
- var init_detect_type = __esmMin((() => {}));
274
- //#endregion
275
125
  //#region src/utils/load-config.ts
276
- async function loadConfig(configPath) {
126
+ async function loadConfig(configPath, bustCache = false) {
277
127
  const absPath = resolve(configPath);
278
128
  if (!existsSync(absPath)) {
279
129
  consola.error(`Config file not found: ${absPath}`);
280
130
  process.exit(1);
281
131
  }
132
+ const importPath = bustCache ? `${absPath}?t=${Date.now()}` : absPath;
282
133
  let mod;
283
134
  try {
284
- mod = await import(absPath);
135
+ mod = await import(importPath);
285
136
  } catch (err) {
286
137
  consola.error(`Failed to load config: ${absPath}`);
287
138
  consola.error(err);
@@ -298,9 +149,31 @@ var init_load_config = __esmMin((() => {}));
298
149
  //#endregion
299
150
  //#region src/commands/generate.ts
300
151
  var generate_exports = /* @__PURE__ */ __exportAll({ generateCommand: () => generateCommand });
152
+ async function generateFromConfig(configPath, formats, outDir, bustCache = false) {
153
+ const config = await loadConfig(configPath, bustCache);
154
+ if (isOpenPolicyConfig(config)) {
155
+ const inputs = expandOpenPolicyConfig(config);
156
+ if (inputs.length === 0) {
157
+ consola.warn(`Unified config has no privacy or terms sections: ${configPath}`);
158
+ return;
159
+ }
160
+ await mkdir(outDir, { recursive: true });
161
+ for (const input of inputs) {
162
+ const outputFilename = input.type === "terms" ? "terms-of-service" : input.type === "cookie" ? "cookie-policy" : "privacy-policy";
163
+ consola.start(`Generating ${input.type} policy from ${configPath} → formats: ${formats.join(", ")}`);
164
+ const results = compilePolicy(input, { formats });
165
+ for (const result of results) {
166
+ const outPath = join(outDir, `${outputFilename}.${result.format === "markdown" ? "md" : result.format}`);
167
+ await writeFile(outPath, result.content, "utf-8");
168
+ consola.success(`Written: ${outPath}`);
169
+ }
170
+ }
171
+ return;
172
+ }
173
+ throw new Error(`[openpolicy] Config must use defineConfig() (OpenPolicyConfig): ${configPath}`);
174
+ }
301
175
  var generateCommand;
302
176
  var init_generate = __esmMin((() => {
303
- init_detect_type();
304
177
  init_load_config();
305
178
  generateCommand = defineCommand({
306
179
  meta: {
@@ -310,8 +183,8 @@ var init_generate = __esmMin((() => {
310
183
  args: {
311
184
  config: {
312
185
  type: "positional",
313
- description: "Path(s) to policy config file(s), comma-separated",
314
- default: "./policy.config.ts,./terms.config.ts"
186
+ description: "Path to policy config file",
187
+ default: "./openpolicy.ts"
315
188
  },
316
189
  format: {
317
190
  type: "string",
@@ -323,44 +196,33 @@ var init_generate = __esmMin((() => {
323
196
  description: "Output directory",
324
197
  default: "./output"
325
198
  },
326
- type: {
327
- type: "string",
328
- description: "Policy type: \"privacy\" or \"terms\" (auto-detected from filename if omitted)",
329
- default: ""
199
+ watch: {
200
+ type: "boolean",
201
+ description: "Watch config file and regenerate on changes",
202
+ default: false
330
203
  }
331
204
  },
332
205
  async run({ args }) {
333
- const formats = (args.format ?? "markdown").split(",").map((f) => f.trim()).filter(Boolean);
334
- const outDir = args.out ?? "./output";
335
- const configPaths = (args.config ?? "./policy.config.ts,./terms.config.ts").split(",").map((p) => p.trim()).filter(Boolean);
336
- const isMulti = configPaths.length > 1;
337
- for (const configPath of configPaths) {
338
- if (!await access(configPath).then(() => true).catch(() => false)) {
339
- if (isMulti) {
340
- consola.warn(`Config not found, skipping: ${configPath}`);
341
- continue;
342
- }
343
- throw new Error(`Config not found: ${configPath}`);
344
- }
345
- const policyType = detectType(args.type || void 0, configPath);
346
- consola.start(`Generating ${policyType} policy from ${configPath} → formats: ${formats.join(", ")}`);
347
- const config = await loadConfig(configPath);
348
- const outputFilename = policyType === "terms" ? "terms-of-service" : "privacy-policy";
349
- const results = compilePolicy(policyType === "terms" ? {
350
- type: "terms",
351
- ...config
352
- } : {
353
- type: "privacy",
354
- ...config
355
- }, { formats });
356
- await mkdir(outDir, { recursive: true });
357
- for (const result of results) {
358
- const outPath = join(outDir, `${outputFilename}.${result.format === "markdown" ? "md" : result.format}`);
359
- await writeFile(outPath, result.content, "utf-8");
360
- consola.success(`Written: ${outPath}`);
361
- }
362
- }
206
+ const formats = args.format.split(",").map((f) => f.trim()).filter(Boolean);
207
+ const outDir = args.out;
208
+ const configPath = args.config;
209
+ if (!existsSync(configPath)) throw new Error(`Config not found: ${configPath}`);
210
+ await generateFromConfig(configPath, formats, outDir);
363
211
  consola.success(`Policy generation complete → ${outDir}`);
212
+ if (args.watch) {
213
+ consola.info("Watching for changes...");
214
+ let debounceTimer = null;
215
+ watch(configPath, () => {
216
+ if (debounceTimer) clearTimeout(debounceTimer);
217
+ debounceTimer = setTimeout(async () => {
218
+ try {
219
+ await generateFromConfig(configPath, formats, outDir, true);
220
+ } catch (err) {
221
+ consola.error(`Error regenerating ${configPath}:`, err);
222
+ }
223
+ }, 100);
224
+ });
225
+ }
364
226
  }
365
227
  });
366
228
  }));
@@ -369,7 +231,6 @@ var init_generate = __esmMin((() => {
369
231
  var validate_exports = /* @__PURE__ */ __exportAll({ validateCommand: () => validateCommand });
370
232
  var validateCommand;
371
233
  var init_validate = __esmMin((() => {
372
- init_detect_type();
373
234
  init_load_config();
374
235
  validateCommand = defineCommand({
375
236
  meta: {
@@ -386,31 +247,29 @@ var init_validate = __esmMin((() => {
386
247
  type: "string",
387
248
  description: "Jurisdiction to validate against: gdpr, ccpa, or all",
388
249
  default: "all"
389
- },
390
- type: {
391
- type: "string",
392
- description: "Policy type: \"privacy\" or \"terms\" (auto-detected from filename if omitted)",
393
- default: ""
394
250
  }
395
251
  },
396
252
  async run({ args }) {
397
253
  const configPath = args.config ?? "./policy.config.ts";
398
- const policyType = detectType(args.type || void 0, configPath);
399
- consola.start(`Validating ${policyType} policy: ${configPath}`);
400
254
  const config = await loadConfig(configPath);
401
- const issues = policyType === "terms" ? validateTermsOfService(config) : validatePrivacyPolicy(config);
402
- if (issues.length === 0) {
403
- consola.success("Config is valid — no issues found.");
404
- return;
405
- }
406
- for (const issue of issues) if (issue.level === "error") consola.error(issue.message);
407
- else consola.warn(issue.message);
408
- const errors = issues.filter((i) => i.level === "error");
409
- if (errors.length > 0) {
410
- consola.fail(`Validation failed with ${errors.length} error(s).`);
411
- process.exit(1);
255
+ if (!isOpenPolicyConfig(config)) throw new Error(`[openpolicy] Config must use defineConfig() (OpenPolicyConfig): ${configPath}`);
256
+ const inputs = expandOpenPolicyConfig(config);
257
+ let totalErrors = 0;
258
+ for (const input of inputs) {
259
+ consola.start(`Validating ${input.type} policy: ${configPath}`);
260
+ const issues = input.type === "terms" ? validateTermsOfService(input) : input.type === "cookie" ? validateCookiePolicy(input) : validatePrivacyPolicy(input);
261
+ if (issues.length === 0) {
262
+ consola.success(`${input.type}: no issues found.`);
263
+ continue;
264
+ }
265
+ for (const issue of issues) if (issue.level === "error") consola.error(issue.message);
266
+ else consola.warn(issue.message);
267
+ const errors = issues.filter((i) => i.level === "error");
268
+ totalErrors += errors.length;
269
+ if (errors.length > 0) consola.fail(`${input.type}: validation failed with ${errors.length} error(s).`);
270
+ else consola.success(`${input.type}: validation passed with warnings.`);
412
271
  }
413
- consola.success("Validation passed with warnings.");
272
+ if (totalErrors > 0) process.exit(1);
414
273
  }
415
274
  });
416
275
  }));
@@ -419,7 +278,7 @@ var init_validate = __esmMin((() => {
419
278
  runMain(defineCommand({
420
279
  meta: {
421
280
  name: "openpolicy",
422
- version: "0.0.1",
281
+ version,
423
282
  description: "Generate and validate privacy policy documents"
424
283
  },
425
284
  subCommands: {
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@openpolicy/cli",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "type": "module",
5
5
  "description": "CLI for generating and validating OpenPolicy privacy policy documents",
6
- "license": "MIT",
6
+ "license": "GPL-3.0-only",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/jamiedavenport/openpolicy",