@kici-dev/sdk 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,8 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
+ import { ChangedFilesUnavailableError, defineChangedFilesGetter, eventTypeOf } from "./changed-files.js";
2
3
  import { $ } from "zx";
3
4
  //#region src/rules/context.ts
4
5
  /**
5
- * Thrown when a rule reads `ctx.changedFiles` but the diff is not available
6
- * (`changedFilesStatus !== 'fetched'`). `evaluateRules` re-throws this rather
7
- * than folding it into a `passed=false` skip, so the job fails loudly instead
8
- * of silently mis-evaluating a path-based gate.
9
- */
10
- var ChangedFilesUnavailableError = class extends Error {
11
- changedFilesStatus;
12
- eventType;
13
- constructor(status, eventType) {
14
- super(`ctx.changedFiles is not available (status: ${status}` + (eventType ? `, event: ${eventType}` : "") + "). Changed files are only defined for push / pull_request events with a computable diff. Guard with ctx.changedFilesStatus before accessing, e.g. `if (ctx.changedFilesStatus !== 'fetched') return true`.");
15
- this.name = "ChangedFilesUnavailableError";
16
- this.changedFilesStatus = status;
17
- this.eventType = eventType;
18
- }
19
- };
20
- /**
21
6
  * Build a RuleContext. `changedFiles` is exposed as a getter: it returns the
22
7
  * list when `changedFilesStatus === 'fetched'`, otherwise it throws
23
8
  * `ChangedFilesUnavailableError`. This is the single construction site for a
@@ -25,23 +10,20 @@ var ChangedFilesUnavailableError = class extends Error {
25
10
  */
26
11
  function createRuleContext(input) {
27
12
  const status = input.changedFilesStatus ?? "fetched";
28
- const files = input.changedFiles ?? [];
29
- const eventType = typeof input.event.type === "string" ? input.event.type : void 0;
30
13
  const base = {
31
14
  event: input.event,
32
15
  changedFilesStatus: status,
33
16
  env: input.env ?? {},
34
17
  dispatchInputs: input.dispatchInputs ?? {},
35
18
  ...input.fanout && { fanout: input.fanout },
19
+ ...input.sourceRepo && { sourceRepo: input.sourceRepo },
20
+ ...input.workflowRepo && { workflowRepo: input.workflowRepo },
36
21
  $
37
22
  };
38
- Object.defineProperty(base, "changedFiles", {
39
- enumerable: true,
40
- configurable: true,
41
- get() {
42
- if (status !== "fetched") throw new ChangedFilesUnavailableError(status, eventType);
43
- return files;
44
- }
23
+ defineChangedFilesGetter(base, {
24
+ files: input.changedFiles ?? [],
25
+ status,
26
+ eventType: eventTypeOf(input.event)
45
27
  });
46
28
  return base;
47
29
  }
@@ -1,5 +1,6 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
- import { ChangedFilesUnavailableError } from "./context.js";
2
+ import { ChangedFilesUnavailableError } from "./changed-files.js";
3
+ import "./context.js";
3
4
  import { toErrorMessage } from "@kici-dev/core";
4
5
  //#region src/rules/evaluator.ts
5
6
  /**
@@ -1,6 +1,7 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
+ import { ChangedFilesUnavailableError } from "./changed-files.js";
2
3
  import { onlyOnFanoutIndex, onlyOnFirstHost, onlyOnLastHost, rule, skip } from "./rule.js";
3
- import { ChangedFilesUnavailableError, createRuleContext } from "./context.js";
4
+ import { createRuleContext } from "./context.js";
4
5
  import { evaluateRules } from "./evaluator.js";
5
6
  import { isEventType } from "../events/event-payloads.js";
6
7
  export { ChangedFilesUnavailableError, createRuleContext, evaluateRules, isEventType, onlyOnFanoutIndex, onlyOnFirstHost, onlyOnLastHost, rule, skip };
@@ -1,6 +1,7 @@
1
1
  import type { $ as Shell } from 'zx';
2
2
  import type { EventPayload } from '../events/event-payloads.js';
3
3
  import type { FanoutPosition } from '../fanout-context.js';
4
+ import type { RepoInfo } from '../context.js';
4
5
  export type { EventPayload } from '../events/event-payloads.js';
5
6
  /**
6
7
  * Context passed to rule check functions.
@@ -21,6 +22,14 @@ export interface RuleContext {
21
22
  changedFiles: string[];
22
23
  /** Availability of `changedFiles` (see `changedFiles`). */
23
24
  changedFilesStatus: import('@kici-dev/engine').ChangedFilesStatus;
25
+ /**
26
+ * The repo whose event triggered this run. Present for a global workflow and
27
+ * for any evaluation that runs with a checkout; undefined otherwise.
28
+ * Read through `.path`; never embed it in a job name (see FilterContext).
29
+ */
30
+ sourceRepo?: RepoInfo;
31
+ /** The repo that registered the workflow. Identical to `sourceRepo` outside a global workflow. */
32
+ workflowRepo?: RepoInfo;
24
33
  /** Environment variables */
25
34
  env: Record<string, string | undefined>;
26
35
  /** Operator-supplied, validated + coerced workflow-dispatch inputs. Empty when none declared. */
@@ -2,8 +2,8 @@ import "../rolldown-runtime-ClRpJifh.js";
2
2
  import { resolveJobOutputs, resolveStepOutputs } from "../outputs.js";
3
3
  import { createStepSecrets } from "../secrets.js";
4
4
  import { isEventDefinition } from "../events/define-event.js";
5
- import { initZx } from "@kici-dev/core";
6
5
  import { $ } from "zx";
6
+ import { initZx } from "@kici-dev/core";
7
7
  import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
8
8
  import { tmpdir } from "node:os";
9
9
  import { dirname, join } from "node:path";
@@ -218,13 +218,15 @@ function createTestStepContext(options = {}) {
218
218
  get: () => Promise.resolve(null)
219
219
  },
220
220
  oidc: { token: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.oidc.token() is not available in a test step context")) },
221
+ git: { github: { getToken: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.git.github.getToken() is not available in a test step context")) } },
221
222
  host: { requestReboot: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.host.requestReboot() is not available in a test step context")) },
222
223
  bootstrap: {
223
224
  ensureInitRunner: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.bootstrap.ensureInitRunner() is not available in a test step context")),
224
225
  preBootSend: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.bootstrap.preBootSend() is not available in a test step context")),
225
226
  agentVersionStatus: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.bootstrap.agentVersionStatus() is not available in a test step context")),
226
227
  restageAgent: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.bootstrap.restageAgent() is not available in a test step context"))
227
- }
228
+ },
229
+ scaler: { claimAgentCredentials: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.scaler.claimAgentCredentials() is not available in a test step context")) }
228
230
  },
229
231
  cache: {
230
232
  restore: async () => ({ hit: false }),
@@ -38,6 +38,8 @@ function pr(config) {
38
38
  sourceBranches: Object.freeze([...sourceBranches]),
39
39
  paths: Object.freeze([...paths]),
40
40
  repos: Object.freeze([...repos]),
41
+ ...config?.requires !== void 0 && { requires: Object.freeze([...config.requires]) },
42
+ ...config?.commitMessage !== void 0 && { commitMessage: config.commitMessage },
41
43
  ...config?.description !== void 0 && { description: config.description }
42
44
  };
43
45
  return Object.freeze(result);
@@ -38,6 +38,8 @@ function push(config) {
38
38
  tags: Object.freeze([...tags]),
39
39
  paths: Object.freeze([...paths]),
40
40
  repos: Object.freeze([...repos]),
41
+ ...config?.requires !== void 0 && { requires: Object.freeze([...config.requires]) },
42
+ ...config?.commitMessage !== void 0 && { commitMessage: config.commitMessage },
41
43
  ...config?.description !== void 0 && { description: config.description }
42
44
  };
43
45
  return Object.freeze(result);
@@ -21,6 +21,8 @@ function tag(config) {
21
21
  _tag: "TagTrigger",
22
22
  patterns: Object.freeze([...patterns]),
23
23
  repos: Object.freeze([...repos]),
24
+ ...config?.requires !== void 0 && { requires: Object.freeze([...config.requires]) },
25
+ ...config?.commitMessage !== void 0 && { commitMessage: config.commitMessage },
24
26
  ...config?.description !== void 0 && { description: config.description }
25
27
  };
26
28
  return Object.freeze(result);
@@ -3,6 +3,8 @@
3
3
  * Supports both glob patterns and regex patterns for branch/path matching.
4
4
  */
5
5
  import type { z } from 'zod';
6
+ import type { ContentRequirement, TextMatch } from '@kici-dev/engine';
7
+ export type { ContentRequirement, ContentFormat, TextMatch } from '@kici-dev/engine';
6
8
  /**
7
9
  * A declared map of typed workflow-dispatch inputs: `{ name: ZodSchema }`.
8
10
  * Each schema must fall within the closed dispatch-input subset (extracted by
@@ -44,6 +46,10 @@ export interface PrTriggerConfig {
44
46
  readonly sourceBranches: readonly BranchPattern[];
45
47
  readonly paths: readonly string[];
46
48
  readonly repos: readonly BranchPattern[];
49
+ /** Declarative static content filter over source files at the event ref (AND-ed). */
50
+ readonly requires?: readonly ContentRequirement[];
51
+ /** Declarative static filter over the event's commit message / PR title+body. */
52
+ readonly commitMessage?: TextMatch;
47
53
  readonly description?: string;
48
54
  }
49
55
  /**
@@ -55,6 +61,10 @@ export interface PushTriggerConfig {
55
61
  readonly tags: readonly BranchPattern[];
56
62
  readonly paths: readonly string[];
57
63
  readonly repos: readonly BranchPattern[];
64
+ /** Declarative static content filter over source files at the event ref (AND-ed). */
65
+ readonly requires?: readonly ContentRequirement[];
66
+ /** Declarative static filter over the event's commit message / PR title+body. */
67
+ readonly commitMessage?: TextMatch;
58
68
  readonly description?: string;
59
69
  }
60
70
  /**
@@ -64,6 +74,10 @@ export interface TagTriggerConfig {
64
74
  readonly _tag: 'TagTrigger';
65
75
  readonly patterns: readonly BranchPattern[];
66
76
  readonly repos: readonly BranchPattern[];
77
+ /** Declarative static content filter over source files at the event ref (AND-ed). */
78
+ readonly requires?: readonly ContentRequirement[];
79
+ /** Declarative static filter over the event's commit message / PR title+body. */
80
+ readonly commitMessage?: TextMatch;
67
81
  readonly description?: string;
68
82
  }
69
83
  /**
@@ -72,6 +86,10 @@ export interface TagTriggerConfig {
72
86
  export interface TagConfigInput {
73
87
  readonly patterns?: string | RegExp | (string | RegExp)[];
74
88
  readonly repos?: string | RegExp | (string | RegExp)[];
89
+ /** Declarative static content filter over source files at the event ref (AND-ed). */
90
+ readonly requires?: readonly ContentRequirement[];
91
+ /** Declarative static filter over the event's commit message / PR title+body. */
92
+ readonly commitMessage?: TextMatch;
75
93
  readonly description?: string;
76
94
  }
77
95
  export type CommentAction = 'created' | 'edited' | 'deleted';
@@ -558,6 +576,10 @@ export interface PrConfigInput {
558
576
  readonly source?: string | RegExp | (string | RegExp)[];
559
577
  readonly paths?: string[];
560
578
  readonly repos?: string | RegExp | (string | RegExp)[];
579
+ /** Declarative static content filter over source files at the event ref (AND-ed). */
580
+ readonly requires?: readonly ContentRequirement[];
581
+ /** Declarative static filter over the event's commit message / PR title+body. */
582
+ readonly commitMessage?: TextMatch;
561
583
  readonly description?: string;
562
584
  }
563
585
  /**
@@ -569,6 +591,10 @@ export interface PushConfigInput {
569
591
  readonly tags?: string | RegExp | (string | RegExp)[];
570
592
  readonly paths?: string[];
571
593
  readonly repos?: string | RegExp | (string | RegExp)[];
594
+ /** Declarative static content filter over source files at the event ref (AND-ed). */
595
+ readonly requires?: readonly ContentRequirement[];
596
+ /** Declarative static filter over the event's commit message / PR title+body. */
597
+ readonly commitMessage?: TextMatch;
572
598
  readonly description?: string;
573
599
  }
574
600
  /**
package/dist/types.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import type { z } from 'zod';
2
2
  import type { $ as Shell } from 'zx';
3
3
  import type { RetryBackoff } from '@kici-dev/core';
4
+ import type { GitCredentialMap, ContainerRegistryAuth } from './git-types.js';
4
5
  import type { ResourceRequest, RunsOnAllInput, OnUnreachableMode, NeedsWhen, ExecutionJobStatus } from '@kici-dev/engine';
5
- import type { StepContext, Logger } from './context.js';
6
+ import type { StepContext, Logger, RepoInfo } from './context.js';
7
+ import type { FilterFn } from './filter.js';
6
8
  import type { SingleNeedEntry, NeedsContext } from './needs-context.js';
7
9
  import type { TriggerConfig } from './triggers/types.js';
8
10
  import type { Rule } from './rules/types.js';
@@ -308,6 +310,14 @@ export interface DynamicJobContext {
308
310
  env: Record<string, string | undefined>;
309
311
  /** Typed KiCI API — orchestrator queries over WS (e.g., kici.infrastructure.list()) */
310
312
  kici: KiciApi;
313
+ /**
314
+ * The repo whose event triggered this run. Present for a global workflow and
315
+ * for any evaluation that runs with a checkout; undefined otherwise.
316
+ * Read through `.path`; never embed it in a job name (see FilterContext).
317
+ */
318
+ sourceRepo?: RepoInfo;
319
+ /** The repo that registered the workflow. Identical to `sourceRepo` outside a global workflow. */
320
+ workflowRepo?: RepoInfo;
311
321
  }
312
322
  /**
313
323
  * Async function that generates jobs dynamically at runtime.
@@ -397,10 +407,43 @@ export { DYNAMIC_JOB_NEEDS_TAG };
397
407
  * When set, all steps run inside the specified container.
398
408
  */
399
409
  export interface ContainerConfig {
400
- /** Docker image name (e.g., 'node:20-alpine') */
401
- image: string;
410
+ /**
411
+ * Docker image name (e.g., 'node:20-alpine'). Mutually exclusive with
412
+ * `dockerfile`; exactly one of the two is required.
413
+ */
414
+ image?: string;
415
+ /**
416
+ * Repo-relative path to a Dockerfile the agent builds before the job runs.
417
+ * Mutually exclusive with `image`.
418
+ *
419
+ * The build runs on the agent host, NOT inside the job's hardened sandbox.
420
+ * See the container-jobs documentation for the trust model and the
421
+ * untrusted-ref rule.
422
+ */
423
+ dockerfile?: string;
424
+ /**
425
+ * Repo-relative build context. Defaults to the repository root.
426
+ * Meaningful only with `dockerfile`.
427
+ */
428
+ context?: string;
429
+ /** Build stage to stop at (`--target`). Meaningful only with `dockerfile`. */
430
+ target?: string;
431
+ /**
432
+ * Build arguments. Meaningful only with `dockerfile`.
433
+ *
434
+ * Plain strings only, deliberately: a build argument is recorded in the
435
+ * image's history, so a secret placed here is readable by anyone who can
436
+ * inspect the image. Pass a secret to a step instead.
437
+ */
438
+ args?: Record<string, string>;
402
439
  /** Additional environment variables for the container */
403
440
  env?: Record<string, string>;
441
+ /**
442
+ * Private-registry credentials. With `image` they pull that image; with
443
+ * `dockerfile` they pull the Dockerfile's own `FROM` base — and must name
444
+ * the registry, because there is no image reference to derive it from.
445
+ */
446
+ auth?: ContainerRegistryAuth;
404
447
  }
405
448
  /**
406
449
  * Per-job sandbox escape hatch for container-sandbox jobs (a job with a
@@ -565,6 +608,13 @@ export interface Job<TOutputs = Record<string, unknown>, TName extends string =
565
608
  readonly maxParallel?: number;
566
609
  /** Halt the fan-out on first child failure, skipping the remainder. Default `false`. */
567
610
  readonly failFast?: boolean;
611
+ /**
612
+ * Invoke the source repo's opt-in workflows and gate on them. Mutually
613
+ * exclusive with `steps` / `run` — an invoke job never runs step code on an
614
+ * agent; it fans out into one proxy node per triggered run. Built by
615
+ * `invokeSource()`.
616
+ */
617
+ readonly invoke?: import('./invoke.js').InvokeConfig;
568
618
  readonly steps: readonly StepInput[];
569
619
  readonly needs?: ReadonlyArray<Job | string | DynamicGroupRef | {
570
620
  name: string;
@@ -585,6 +635,17 @@ export interface Job<TOutputs = Record<string, unknown>, TName extends string =
585
635
  readonly exclude?: MatrixExclude[];
586
636
  /** When false, agent skips git clone (default: true). Useful for deploy/notify jobs. */
587
637
  readonly checkout?: boolean;
638
+ /**
639
+ * Named git credentials available to every step in this job.
640
+ *
641
+ * Values are **secret names**, resolved from the secrets backend — never
642
+ * secret material. `default` is used whenever a call names no credential;
643
+ * any other key is referenced by name, e.g.
644
+ * `kici.git.clone({ repo, credential: 'forge' })`.
645
+ *
646
+ * Omit entirely and the source credential applies, which is all a read needs.
647
+ */
648
+ readonly gitCredentials?: GitCredentialMap;
588
649
  /** Docker image for job execution. All steps run inside the container. */
589
650
  readonly container?: string | ContainerConfig;
590
651
  /** Per-job sandbox escape hatch (container jobs only); granted within the operator allow-list. */
@@ -700,6 +761,13 @@ export interface JobOptions {
700
761
  * of sibling outcomes). Applies to both matrix and `runsOnAll` fan-out.
701
762
  */
702
763
  failFast?: boolean;
764
+ /**
765
+ * Invoke the source repo's opt-in workflows and gate on them. Mutually
766
+ * exclusive with `steps` / `run` — an invoke job never runs step code on an
767
+ * agent; it fans out into one proxy node per triggered run. Built by
768
+ * `invokeSource()`.
769
+ */
770
+ invoke?: import('./invoke.js').InvokeConfig;
703
771
  /**
704
772
  * Steps to execute in this job. Accepts Step objects and bare async functions.
705
773
  * Mutually exclusive with `run`.
@@ -740,6 +808,17 @@ export interface JobOptions {
740
808
  exclude?: MatrixExclude[];
741
809
  /** When false, agent skips git clone (default: true). Useful for deploy/notify jobs. */
742
810
  checkout?: boolean;
811
+ /**
812
+ * Named git credentials available to every step in this job.
813
+ *
814
+ * Values are **secret names**, resolved from the secrets backend — never
815
+ * secret material. `default` is used whenever a call names no credential;
816
+ * any other key is referenced by name, e.g.
817
+ * `kici.git.clone({ repo, credential: 'forge' })`.
818
+ *
819
+ * Omit entirely and the source credential applies, which is all a read needs.
820
+ */
821
+ gitCredentials?: GitCredentialMap;
743
822
  /**
744
823
  * Docker image for job execution.
745
824
  * Simple string form for image name, object form for additional config.
@@ -845,6 +924,23 @@ export interface Workflow {
845
924
  readonly on?: TriggerConfig[];
846
925
  /** Rules for conditional execution */
847
926
  readonly rules?: Rule[];
927
+ /**
928
+ * Pre-dispatch predicate deciding whether this workflow applies to the event's
929
+ * source repo. A `false` result suppresses the workflow's jobs — none is
930
+ * dispatched, and none is reported as skipped.
931
+ *
932
+ * An organization-wide workflow is evaluated once per (event × workflow repo)
933
+ * before any run row is created, so a `false` verdict produces no run. A
934
+ * same-repo workflow is evaluated once per job and once per job generator,
935
+ * after the run row exists — so a run is recorded showing the evaluation and
936
+ * none of the workflow's own jobs. Because it runs once per job there, the
937
+ * predicate must be cheap, side-effect free, and deterministic; see `FilterFn`.
938
+ *
939
+ * One same-repo exception: a job **held for approval** or **rejected by a
940
+ * context rule** is not filtered at all. The hold or the rule is its gate, so
941
+ * an approved job dispatches with no filter verdict having been taken.
942
+ */
943
+ readonly filter?: FilterFn;
848
944
  /** Optional description */
849
945
  readonly description?: string;
850
946
  /**
@@ -899,6 +995,26 @@ export interface WorkflowOptions {
899
995
  on?: Trigger | Trigger[];
900
996
  /** Rules that must pass for workflow to execute */
901
997
  rules?: Rule[];
998
+ /**
999
+ * Pre-dispatch predicate deciding whether this workflow applies to the event's
1000
+ * source repo. Runs on an evaluating agent with the tree(s) on disk, and a
1001
+ * `false` result suppresses the workflow's jobs — none is dispatched, and none
1002
+ * is reported as skipped.
1003
+ *
1004
+ * An organization-wide workflow is evaluated once per (event × workflow repo)
1005
+ * before any run row is created, so a `false` verdict produces no run. A
1006
+ * same-repo workflow is evaluated once per job and once per job generator,
1007
+ * after the run row exists — so a run is recorded showing the evaluation and
1008
+ * none of the workflow's own jobs. Because it runs once per job there, the
1009
+ * predicate must be cheap, side-effect free, and deterministic; see `FilterFn`.
1010
+ *
1011
+ * One same-repo exception: a job **held for approval** or **rejected by a
1012
+ * context rule** is not filtered at all. The hold or the rule is its gate, so
1013
+ * an approved job dispatches with no filter verdict having been taken.
1014
+ *
1015
+ * See `FilterContext` for what it receives and what it deliberately does not.
1016
+ */
1017
+ filter?: FilterFn;
902
1018
  /** Optional description for documentation */
903
1019
  description?: string;
904
1020
  /**
package/dist/workflow.js CHANGED
@@ -60,12 +60,14 @@ function workflow(name, options) {
60
60
  if (options.installEnv) {
61
61
  for (const [i, ref] of options.installEnv.entries()) if (!isQualifiedSecretRef(ref)) throw new Error(`workflow('${name}'): installEnv[${i}] must use qualified <context>:<secret-name> syntax (got: ${ref})`);
62
62
  }
63
+ if (options.filter !== void 0 && typeof options.filter !== "function") throw new Error(`workflow('${name}'): filter must be a function (got: ${typeof options.filter})`);
63
64
  return {
64
65
  _tag: "Workflow",
65
66
  name,
66
67
  jobs: options.jobs,
67
68
  on: normalizeTriggers(options.on),
68
69
  rules: options.rules,
70
+ filter: options.filter,
69
71
  description: options.description,
70
72
  hashFiles: options.hashFiles,
71
73
  registries: options.registries,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kici-dev/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "TypeScript SDK for defining KiCI workflows. Import into `.kici/workflows/*.ts` to declare workflows, jobs, steps, triggers, rules, and matrix configurations.",
5
5
  "keywords": [
6
6
  "ci",
@@ -52,10 +52,11 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "micromatch": "^4.0.8",
55
+ "yaml": "^2.9.0",
55
56
  "zod": "^4.4.3",
56
57
  "zx": "^8.8.5",
57
- "@kici-dev/core": "0.4.0",
58
- "@kici-dev/engine": "0.4.0"
58
+ "@kici-dev/core": "0.6.0",
59
+ "@kici-dev/engine": "0.6.0"
59
60
  },
60
61
  "devDependencies": {
61
62
  "@types/micromatch": "^4.0.10"