sf-plugin-permission-sets 0.0.0-dev.11 → 0.0.0-dev.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.
package/README.md CHANGED
@@ -196,11 +196,12 @@ Fully offline: runs in any CI job or pre-commit hook without org credentials.
196
196
 
197
197
  ```
198
198
  USAGE
199
- $ sf ps check -f <glob>... [--strict] [--json]
199
+ $ sf ps check -f <glob>... [--strict] [--watch] [--json]
200
200
 
201
201
  FLAGS
202
202
  -f, --file=<glob>... (required) YAML file(s) to read. Repeatable, globs are expanded by the plugin.
203
203
  --strict Treat warnings as errors.
204
+ -w, --watch Re-run on every change to a matched file. Stays in the foreground until you stop it (Ctrl-C).
204
205
 
205
206
  CHECKS
206
207
  - valid YAML & schema (unknown keys rejected)
@@ -210,15 +211,18 @@ CHECKS
210
211
  - internal referential integrity
211
212
  ```
212
213
 
214
+ `--watch` is for the local edit loop: leave it running while you tweak the YAML and the findings refresh on every save. It re-expands the globs each run, so files you add or delete are picked up too. It's a foreground, interactive mode, so it can't be combined with `--json` and isn't meant for CI, where a single gated `check` run is what you want.
215
+
213
216
  ### `sf ps validate`
214
217
 
215
218
  ```
216
219
  USAGE
217
- $ sf ps validate -o <org> -f <glob>... [--json]
220
+ $ sf ps validate -o <org> -f <glob>... [--watch] [--json]
218
221
 
219
222
  FLAGS
220
223
  -o, --target-org=<org> (required) Org to resolve against.
221
224
  -f, --file=<glob>... (required) YAML file(s) to read. Repeatable, globs expanded by the plugin.
225
+ -w, --watch Re-run on every change to a matched file. Read-only, but each run queries the org, so point it at a scratch or dev org.
222
226
 
223
227
  Runs all offline checks, then verifies that every user (active), permission set,
224
228
  group, and license referenced actually exists and resolves uniquely.
@@ -228,13 +232,14 @@ group, and license referenced actually exists and resolves uniquely.
228
232
 
229
233
  ```
230
234
  USAGE
231
- $ sf ps plan -o <org> -f <glob>... [--mode <value>] [--fail-on-drift] [--json]
235
+ $ sf ps plan -o <org> -f <glob>... [--mode <value>] [--fail-on-drift] [--watch] [--json]
232
236
 
233
237
  FLAGS
234
238
  -o, --target-org=<org> (required)
235
239
  -f, --file=<glob>... (required) YAML file(s) to read. Repeatable, globs expanded by the plugin.
236
240
  --mode=<value> additive | destructive | sync [default: additive]
237
241
  --fail-on-drift Exit non-zero if any change is pending (for CI gates).
242
+ -w, --watch Re-diff against the org on every change to a matched file. Read-only (never applies), but each run queries the org.
238
243
  ```
239
244
 
240
245
  Example output:
@@ -0,0 +1,18 @@
1
+ import { SfCommand } from '@salesforce/sf-plugins-core';
2
+ import { Finding } from '../../core/model.js';
3
+ export type PsCheckResult = {
4
+ files: number;
5
+ users: number;
6
+ assignments: number;
7
+ findings: Finding[];
8
+ };
9
+ export default class Check extends SfCommand<PsCheckResult> {
10
+ static readonly summary: string;
11
+ static readonly description: string;
12
+ static readonly examples: string[];
13
+ static readonly flags: {
14
+ file: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
15
+ strict: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ };
17
+ run(): Promise<PsCheckResult>;
18
+ }
@@ -0,0 +1,45 @@
1
+ import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
2
+ import { Messages } from '@salesforce/core';
3
+ import { CheckService } from '../../services/check.js';
4
+ import { formatFindings } from '../../core/report.js';
5
+ Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
6
+ const messages = Messages.loadMessages('sf-plugin-permission-sets', 'ps.check');
7
+ export default class Check extends SfCommand {
8
+ static summary = messages.getMessage('summary');
9
+ static description = messages.getMessage('description');
10
+ static examples = messages.getMessages('examples');
11
+ static flags = {
12
+ file: Flags.string({
13
+ char: 'f',
14
+ summary: messages.getMessage('flags.file.summary'),
15
+ required: true,
16
+ multiple: true,
17
+ }),
18
+ strict: Flags.boolean({
19
+ summary: messages.getMessage('flags.strict.summary'),
20
+ }),
21
+ };
22
+ async run() {
23
+ const { flags } = await this.parse(Check);
24
+ const result = await new CheckService().run({ files: flags.file, strict: flags.strict });
25
+ for (const line of formatFindings(result.findings)) {
26
+ this.log(line);
27
+ }
28
+ const users = new Set(result.assignments.map((a) => a.assignee)).size;
29
+ this.log('');
30
+ this.log(messages.getMessage('summary.counts', [String(result.errors), String(result.warnings)]));
31
+ if (result.failed) {
32
+ process.exitCode = 1;
33
+ if (!this.jsonEnabled()) {
34
+ this.error(messages.getMessage('error.failed'), { exit: 1 });
35
+ }
36
+ }
37
+ return {
38
+ files: result.files.length,
39
+ users,
40
+ assignments: result.assignments.length,
41
+ findings: result.findings,
42
+ };
43
+ }
44
+ }
45
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../../../src/commands/ps/check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGtD,QAAQ,CAAC,kCAAkC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC7D,MAAM,QAAQ,GAAG,QAAQ,CAAC,YAAY,CAAC,2BAA2B,EAAE,UAAU,CAAC,CAAC;AAShF,MAAM,CAAC,OAAO,OAAO,KAAM,SAAQ,SAAwB;IAClD,MAAM,CAAU,OAAO,GAAG,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IACzD,MAAM,CAAU,WAAW,GAAG,QAAQ,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IACjE,MAAM,CAAU,QAAQ,GAAG,QAAQ,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;IAE5D,MAAM,CAAU,KAAK,GAAG;QAC7B,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC;YACjB,IAAI,EAAE,GAAG;YACT,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,oBAAoB,CAAC;YAClD,QAAQ,EAAE,IAAI;YACd,QAAQ,EAAE,IAAI;SACf,CAAC;QACF,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC;YACpB,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,sBAAsB,CAAC;SACrD,CAAC;KACH,CAAC;IAEK,KAAK,CAAC,GAAG;QACd,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC1C,MAAM,MAAM,GAAG,MAAM,IAAI,YAAY,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;QAEzF,KAAK,MAAM,IAAI,IAAI,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YACnD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjB,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;QACtE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,gBAAgB,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAElG,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAClB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;YAC/D,CAAC;QACH,CAAC;QAED,OAAO;YACL,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM;YAC1B,KAAK;YACL,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM;YACtC,QAAQ,EAAE,MAAM,CAAC,QAAQ;SAC1B,CAAC;IACJ,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { DesiredAssignment, Finding, LoadResult } from './model.js';
2
+ /** Process one file's text through parse, validate, and normalize. Pure, no disk. */
3
+ export declare function checkContent(text: string, file: string): {
4
+ assignments: DesiredAssignment[];
5
+ findings: Finding[];
6
+ };
7
+ /** Expand the globs, read every matched file, and merge into one model by union. */
8
+ export declare function loadFiles(patterns: string[]): Promise<LoadResult>;
@@ -0,0 +1,52 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { globby } from 'globby';
3
+ import { parseFile } from './parse.js';
4
+ import { validateFile } from './schema.js';
5
+ import { normalize } from './normalize.js';
6
+ /** Process one file's text through parse, validate, and normalize. Pure, no disk. */
7
+ export function checkContent(text, file) {
8
+ const parsed = parseFile(text, file);
9
+ if (!parsed.data) {
10
+ return { assignments: [], findings: parsed.findings };
11
+ }
12
+ const validated = validateFile(parsed.data, file);
13
+ if (!validated.data) {
14
+ return { assignments: [], findings: [...parsed.findings, ...validated.findings] };
15
+ }
16
+ const normalized = normalize(validated.data, file);
17
+ return {
18
+ assignments: normalized.assignments,
19
+ findings: [...parsed.findings, ...validated.findings, ...normalized.findings],
20
+ };
21
+ }
22
+ /** Expand the globs, read every matched file, and merge into one model by union. */
23
+ export async function loadFiles(patterns) {
24
+ const files = await globby(patterns);
25
+ if (files.length === 0) {
26
+ return {
27
+ files,
28
+ assignments: [],
29
+ findings: [{ level: 'error', code: 'NO_FILES', message: `no files matched: ${patterns.join(', ')}` }],
30
+ };
31
+ }
32
+ const findings = [];
33
+ const collected = [];
34
+ for (const file of files) {
35
+ // eslint-disable-next-line no-await-in-loop
36
+ const text = await readFile(file, 'utf8');
37
+ const res = checkContent(text, file);
38
+ findings.push(...res.findings);
39
+ collected.push(...res.assignments);
40
+ }
41
+ const seen = new Set();
42
+ const assignments = [];
43
+ for (const a of collected) {
44
+ const dedupeKey = `${a.assignee} ${a.kind} ${a.target}`;
45
+ if (seen.has(dedupeKey))
46
+ continue;
47
+ seen.add(dedupeKey);
48
+ assignments.push(a);
49
+ }
50
+ return { files, assignments, findings };
51
+ }
52
+ //# sourceMappingURL=load.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.js","sourceRoot":"","sources":["../../src/core/load.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,qFAAqF;AACrF,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,IAAY;IACrD,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACrC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACjB,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;IACxD,CAAC;IAED,MAAM,SAAS,GAAG,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAClD,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;QACpB,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,EAAE,GAAG,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;IACpF,CAAC;IAED,MAAM,UAAU,GAAG,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACnD,OAAO;QACL,WAAW,EAAE,UAAU,CAAC,WAAW;QACnC,QAAQ,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,EAAE,GAAG,SAAS,CAAC,QAAQ,EAAE,GAAG,UAAU,CAAC,QAAQ,CAAC;KAC9E,CAAC;AACJ,CAAC;AAED,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,QAAkB;IAChD,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;IACrC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO;YACL,KAAK;YACL,WAAW,EAAE,EAAE;YACf,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,qBAAqB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;SACtG,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,SAAS,GAAwB,EAAE,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,4CAA4C;QAC5C,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC1C,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACrC,QAAQ,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC/B,SAAS,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,WAAW,CAAC,CAAC;IACrC,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,WAAW,GAAwB,EAAE,CAAC;IAC5C,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1B,MAAM,SAAS,GAAG,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;QACxD,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;YAAE,SAAS;QAClC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACpB,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACtB,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,19 @@
1
+ export type Kind = 'permissionSet' | 'permissionSetGroup' | 'permissionSetLicense';
2
+ export type DesiredAssignment = {
3
+ assignee: string;
4
+ kind: Kind;
5
+ target: string;
6
+ };
7
+ export type FindingLevel = 'error' | 'warning';
8
+ export type Finding = {
9
+ level: FindingLevel;
10
+ code: string;
11
+ message: string;
12
+ file?: string;
13
+ line?: number;
14
+ };
15
+ export type LoadResult = {
16
+ files: string[];
17
+ assignments: DesiredAssignment[];
18
+ findings: Finding[];
19
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/core/model.ts"],"names":[],"mappings":""}
@@ -0,0 +1,10 @@
1
+ import { FileShape } from './schema.js';
2
+ import { DesiredAssignment, Finding } from './model.js';
3
+ /**
4
+ * Turn a validated file into canonical (assignee, kind, target) tuples, and
5
+ * emit the structural findings: duplicate targets, empty lists, empty users.
6
+ */
7
+ export declare function normalize(data: FileShape, file: string): {
8
+ assignments: DesiredAssignment[];
9
+ findings: Finding[];
10
+ };
@@ -0,0 +1,45 @@
1
+ const KIND_KEYS = [
2
+ ['permissionSet', 'permissionSets'],
3
+ ['permissionSetGroup', 'permissionSetGroups'],
4
+ ['permissionSetLicense', 'permissionSetLicenses'],
5
+ ];
6
+ /**
7
+ * Turn a validated file into canonical (assignee, kind, target) tuples, and
8
+ * emit the structural findings: duplicate targets, empty lists, empty users.
9
+ */
10
+ export function normalize(data, file) {
11
+ const assignments = [];
12
+ const findings = [];
13
+ for (const [username, entry] of Object.entries(data.users)) {
14
+ let scopeCount = 0;
15
+ for (const [kind, key] of KIND_KEYS) {
16
+ const list = entry[key];
17
+ if (list === undefined)
18
+ continue;
19
+ if (list.length === 0) {
20
+ findings.push({ level: 'warning', code: 'EMPTY_LIST', message: `${username}: ${key} is empty`, file });
21
+ continue;
22
+ }
23
+ scopeCount += 1;
24
+ const seen = new Set();
25
+ for (const target of list) {
26
+ if (seen.has(target)) {
27
+ findings.push({
28
+ level: 'warning',
29
+ code: 'DUP_TARGET',
30
+ message: `${username}: ${target} is listed twice under ${key}`,
31
+ file,
32
+ });
33
+ continue;
34
+ }
35
+ seen.add(target);
36
+ assignments.push({ assignee: username, kind, target });
37
+ }
38
+ }
39
+ if (scopeCount === 0) {
40
+ findings.push({ level: 'warning', code: 'EMPTY_USER', message: `${username}: no scopes declared`, file });
41
+ }
42
+ }
43
+ return { assignments, findings };
44
+ }
45
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.js","sourceRoot":"","sources":["../../src/core/normalize.ts"],"names":[],"mappings":"AAKA,MAAM,SAAS,GAA4B;IACzC,CAAC,eAAe,EAAE,gBAAgB,CAAC;IACnC,CAAC,oBAAoB,EAAE,qBAAqB,CAAC;IAC7C,CAAC,sBAAsB,EAAE,uBAAuB,CAAC;CAClD,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAe,EAAE,IAAY;IACrD,MAAM,WAAW,GAAwB,EAAE,CAAC;IAC5C,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,KAAK,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3D,IAAI,UAAU,GAAG,CAAC,CAAC;QAEnB,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,SAAS,EAAE,CAAC;YACpC,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;YACxB,IAAI,IAAI,KAAK,SAAS;gBAAE,SAAS;YACjC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,GAAG,QAAQ,KAAK,GAAG,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;gBACvG,SAAS;YACX,CAAC;YAED,UAAU,IAAI,CAAC,CAAC;YAChB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;YAC/B,KAAK,MAAM,MAAM,IAAI,IAAI,EAAE,CAAC;gBAC1B,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;oBACrB,QAAQ,CAAC,IAAI,CAAC;wBACZ,KAAK,EAAE,SAAS;wBAChB,IAAI,EAAE,YAAY;wBAClB,OAAO,EAAE,GAAG,QAAQ,KAAK,MAAM,0BAA0B,GAAG,EAAE;wBAC9D,IAAI;qBACL,CAAC,CAAC;oBACH,SAAS;gBACX,CAAC;gBACD,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBACjB,WAAW,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YACzD,CAAC;QACH,CAAC;QAED,IAAI,UAAU,KAAK,CAAC,EAAE,CAAC;YACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,GAAG,QAAQ,sBAAsB,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5G,CAAC;IACH,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;AACnC,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { Finding } from './model.js';
2
+ /**
3
+ * Read one file's text into a plain object. Reports invalid YAML and duplicate
4
+ * keys (uniqueKeys), and treats an empty document as a warning.
5
+ */
6
+ export declare function parseFile(text: string, file: string): {
7
+ data?: unknown;
8
+ findings: Finding[];
9
+ };
@@ -0,0 +1,25 @@
1
+ import { parseDocument } from 'yaml';
2
+ /**
3
+ * Read one file's text into a plain object. Reports invalid YAML and duplicate
4
+ * keys (uniqueKeys), and treats an empty document as a warning.
5
+ */
6
+ export function parseFile(text, file) {
7
+ const doc = parseDocument(text, { uniqueKeys: true });
8
+ if (doc.errors.length > 0) {
9
+ return {
10
+ findings: doc.errors.map((err) => ({
11
+ level: 'error',
12
+ code: 'YAML',
13
+ message: err.message,
14
+ file,
15
+ line: err.linePos?.[0]?.line,
16
+ })),
17
+ };
18
+ }
19
+ const data = doc.toJS();
20
+ if (data === null || data === undefined) {
21
+ return { findings: [{ level: 'warning', code: 'EMPTY_FILE', message: 'file is empty', file }] };
22
+ }
23
+ return { data, findings: [] };
24
+ }
25
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/core/parse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAGrC;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,IAAY;IAClD,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IAEtD,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,OAAO;YACL,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBACjC,KAAK,EAAE,OAAO;gBACd,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE,GAAG,CAAC,OAAO;gBACpB,IAAI;gBACJ,IAAI,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI;aAC7B,CAAC,CAAC;SACJ,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAa,CAAC;IACnC,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACxC,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAClG,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;AAChC,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { Finding } from './model.js';
2
+ /** Render findings as human-readable lines. Shared by check, validate, and plan. */
3
+ export declare function formatFindings(findings: Finding[]): string[];
4
+ /** Count findings by level. */
5
+ export declare function countFindings(findings: Finding[]): {
6
+ errors: number;
7
+ warnings: number;
8
+ };
@@ -0,0 +1,15 @@
1
+ /** Render findings as human-readable lines. Shared by check, validate, and plan. */
2
+ export function formatFindings(findings) {
3
+ return findings.map((finding) => {
4
+ const where = finding.file ? `${finding.file}${finding.line ? `:${finding.line}` : ''} ` : '';
5
+ return `${finding.level}: ${where}${finding.message}`;
6
+ });
7
+ }
8
+ /** Count findings by level. */
9
+ export function countFindings(findings) {
10
+ return {
11
+ errors: findings.filter((f) => f.level === 'error').length,
12
+ warnings: findings.filter((f) => f.level === 'warning').length,
13
+ };
14
+ }
15
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/core/report.ts"],"names":[],"mappings":"AAEA,oFAAoF;AACpF,MAAM,UAAU,cAAc,CAAC,QAAmB;IAChD,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QAC9B,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9F,OAAO,GAAG,OAAO,CAAC,KAAK,KAAK,KAAK,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,+BAA+B;AAC/B,MAAM,UAAU,aAAa,CAAC,QAAmB;IAC/C,OAAO;QACL,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,MAAM;QAC1D,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,MAAM;KAC/D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ import { Finding } from './model.js';
3
+ export declare const userEntrySchema: z.ZodObject<{
4
+ permissionSets: z.ZodOptional<z.ZodArray<z.ZodString>>;
5
+ permissionSetGroups: z.ZodOptional<z.ZodArray<z.ZodString>>;
6
+ permissionSetLicenses: z.ZodOptional<z.ZodArray<z.ZodString>>;
7
+ }, z.core.$strict>;
8
+ export declare const fileSchema: z.ZodObject<{
9
+ users: z.ZodRecord<z.ZodString, z.ZodObject<{
10
+ permissionSets: z.ZodOptional<z.ZodArray<z.ZodString>>;
11
+ permissionSetGroups: z.ZodOptional<z.ZodArray<z.ZodString>>;
12
+ permissionSetLicenses: z.ZodOptional<z.ZodArray<z.ZodString>>;
13
+ }, z.core.$strict>>;
14
+ }, z.core.$strict>;
15
+ export type FileShape = z.infer<typeof fileSchema>;
16
+ /** Validate a parsed object against the file contract, turning issues into findings. */
17
+ export declare function validateFile(data: unknown, file: string): {
18
+ data?: FileShape;
19
+ findings: Finding[];
20
+ };
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ const scopeList = z.array(z.string().min(1)).optional();
3
+ export const userEntrySchema = z.strictObject({
4
+ permissionSets: scopeList,
5
+ permissionSetGroups: scopeList,
6
+ permissionSetLicenses: scopeList,
7
+ });
8
+ export const fileSchema = z.strictObject({
9
+ users: z.record(z.string().min(1), userEntrySchema),
10
+ });
11
+ /** Validate a parsed object against the file contract, turning issues into findings. */
12
+ export function validateFile(data, file) {
13
+ const parsed = fileSchema.safeParse(data);
14
+ if (parsed.success) {
15
+ return { data: parsed.data, findings: [] };
16
+ }
17
+ return {
18
+ findings: parsed.error.issues.map((issue) => ({
19
+ level: 'error',
20
+ code: 'SCHEMA',
21
+ message: `${issue.path.join('.') || '(root)'}: ${issue.message}`,
22
+ file,
23
+ })),
24
+ };
25
+ }
26
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/core/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;AAExD,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,YAAY,CAAC;IAC5C,cAAc,EAAE,SAAS;IACzB,mBAAmB,EAAE,SAAS;IAC9B,qBAAqB,EAAE,SAAS;CACjC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,YAAY,CAAC;IACvC,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,eAAe,CAAC;CACpD,CAAC,CAAC;AAIH,wFAAwF;AACxF,MAAM,UAAU,YAAY,CAAC,IAAa,EAAE,IAAY;IACtD,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAC7C,CAAC;IACD,OAAO;QACL,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAC5C,KAAK,EAAE,OAAO;YACd,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,KAAK,KAAK,CAAC,OAAO,EAAE;YAChE,IAAI;SACL,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { DesiredAssignment, Finding } from '../core/model.js';
2
+ export type CheckOptions = {
3
+ files: string[];
4
+ strict: boolean;
5
+ };
6
+ export type CheckResult = {
7
+ files: string[];
8
+ assignments: DesiredAssignment[];
9
+ findings: Finding[];
10
+ errors: number;
11
+ warnings: number;
12
+ failed: boolean;
13
+ };
14
+ /** Offline check: load the files, validate them, and summarize the findings. */
15
+ export declare class CheckService {
16
+ run(options: CheckOptions): Promise<CheckResult>;
17
+ }
@@ -0,0 +1,20 @@
1
+ import { loadFiles } from '../core/load.js';
2
+ import { countFindings } from '../core/report.js';
3
+ /** Offline check: load the files, validate them, and summarize the findings. */
4
+ export class CheckService {
5
+ // eslint-disable-next-line class-methods-use-this
6
+ async run(options) {
7
+ const loaded = await loadFiles(options.files);
8
+ const { errors, warnings } = countFindings(loaded.findings);
9
+ const failed = errors > 0 || (options.strict && warnings > 0);
10
+ return {
11
+ files: loaded.files,
12
+ assignments: loaded.assignments,
13
+ findings: loaded.findings,
14
+ errors,
15
+ warnings,
16
+ failed,
17
+ };
18
+ }
19
+ }
20
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/services/check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAiBlD,gFAAgF;AAChF,MAAM,OAAO,YAAY;IACvB,kDAAkD;IAC3C,KAAK,CAAC,GAAG,CAAC,OAAqB;QACpC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC9C,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC5D,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,QAAQ,GAAG,CAAC,CAAC,CAAC;QAE9D,OAAO;YACL,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,MAAM;YACN,QAAQ;YACR,MAAM;SACP,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,33 @@
1
+ # summary
2
+
3
+ Statically check permission set assignment files, with no org connection.
4
+
5
+ # description
6
+
7
+ Parse and validate the YAML files offline. Reports invalid YAML, schema violations, duplicate keys, duplicate targets, and empty entries. Run it in a pre-commit hook or in CI before validate, plan, or apply.
8
+
9
+ # flags.file.summary
10
+
11
+ YAML file or glob to check. Repeatable.
12
+
13
+ # flags.strict.summary
14
+
15
+ Treat warnings as errors.
16
+
17
+ # summary.counts
18
+
19
+ %s errors, %s warnings.
20
+
21
+ # error.failed
22
+
23
+ Check found problems. See the output above.
24
+
25
+ # examples
26
+
27
+ - Check every file under permissions:
28
+
29
+ <%= config.bin %> <%= command.id %> --file "permissions/*.yml"
30
+
31
+ - Check specific files:
32
+
33
+ <%= config.bin %> <%= command.id %> --file permissions/sales.yml --file permissions/service.yml