diff --git a/.gitignore b/.gitignore index 5737ef7d..301b8de4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,13 @@ *.so *.dylib +# Node +node_modules + # Artifacts *.exe -# Arhives +# Archives *.tar *.zip diff --git a/docs/man/schema_build.md b/docs/man/schema_build.md new file mode 100644 index 00000000..4f169f20 --- /dev/null +++ b/docs/man/schema_build.md @@ -0,0 +1,74 @@ +# JSON Schema Build + +`docs/spec/schema.tsp` is the TypeSpec source used to generate the editor-facing JSON Schema for `gilbert.yml`. + +## Generate Schema + +Run: + +```sh +npx tsp compile docs/spec/schema.tsp \ + --emit=@typespec/json-schema \ + --option @typespec/json-schema.file-type=json \ + --list-files +``` + +This emits: + +```text +tsp-output/@typespec/json-schema/WorkflowFile.json +``` + +The generated file can be renamed to: + +```text +tsp-output/@typespec/json-schema/gilbert.schema.json +``` + +To write the generated schema into a custom directory, set the JSON Schema emitter output directory: + +```sh +npx tsp compile docs/spec/schema.tsp \ + --emit=@typespec/json-schema \ + --option @typespec/json-schema.file-type=json \ + --option @typespec/json-schema.emitter-output-dir='{cwd}/docs/spec/json-schema' \ + --list-files +``` + +With this option, the generated schema is written to: + +```text +docs/spec/json-schema/WorkflowFile.json +``` + +`WorkflowFile` is the only TypeSpec model decorated with `@jsonSchema`, so helper types are emitted into the same JSON Schema document under `$defs`. This keeps the schema unified and avoids external `*.json` references. + +## Post-Processing + +TypeSpec emits map-like records with `unevaluatedProperties`: + +```json +{ + "type": "object", + "unevaluatedProperties": { + "$ref": "#/$defs/Task" + } +} +``` + +This is valid JSON Schema 2020-12, but YAML language servers have weak support for `unevaluatedProperties` and fail to provide hover/completion for arbitrary nested keys inside those maps. The `additionalProperties` keyword is handled better by YAML language servers. + +To fix this, run the post-processing script to replace `unevaluatedProperties` with `additionalProperties` throughout the schema: + +```sh +node tools/post-process-schema.mjs [outputFile] +``` + +- ``: the generated JSON Schema file +- `[outputFile]`: optional output path; if omitted, writes to stdout + +After generating and patching the schema, reference it from YAML: + +```yaml +# yaml-language-server: $schema=http://localhost:8000/json-schema/gilbert.schema.json +``` \ No newline at end of file diff --git a/docs/spec/.gitignore b/docs/spec/.gitignore new file mode 100644 index 00000000..0db07c74 --- /dev/null +++ b/docs/spec/.gitignore @@ -0,0 +1,6 @@ +# Ignore json schema file produced by TypeSpec. +# It is replaced by a schema patcher script with 'gilbert.schema.json' file. +WorkflowFile.json + +# AI brainstorming artifacts +prompts diff --git a/docs/spec/gilbert.schema.json b/docs/spec/gilbert.schema.json new file mode 100644 index 00000000..9fd46323 --- /dev/null +++ b/docs/spec/gilbert.schema.json @@ -0,0 +1,469 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gilbert.schema.json", + "type": "object", + "properties": { + "version": { + "type": "number", + "const": 2, + "description": "Workflow file version. `2` is the only supported value; other values are invalid." + }, + "include": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of other workflow files to include and merge.\nPaths resolve relative to the including file. A file cannot include\nitself. Duplicate task, mixin, and input names across included files are\nrejected." + }, + "plugins": { + "$ref": "#/$defs/RecordString", + "description": "Set of plugins to import.\nKey is a namespace and value is import URL.\n\nThe namespace becomes the action prefix. For example, a plugin declared as\n`docker:` exposes actions such as `docker/build` and `docker/run`." + }, + "const": { + "$ref": "#/$defs/RecordUnknown", + "description": "Predefined variables available to expressions as `${{ consts.* }}`.\nExpressions are not expanded inside this block. Values are literal YAML\nscalars." + }, + "inputs": { + "type": "object", + "properties": {}, + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/ListInputDefinition" + }, + { + "$ref": "#/$defs/ScalarInputDefinition" + } + ] + }, + "description": "Global input parameters." + }, + "tasks": { + "$ref": "#/$defs/RecordTask", + "description": "Map of task name and its definition.\nTasks are command-line entry points and can be run with\n`gilbert run `. Comments immediately above a task key are collected\nas the task description for list/help output." + }, + "mixins": { + "$ref": "#/$defs/RecordMixin", + "description": "Mixins are reusable pieces of pipeline which can accept inputs.\nUnlike tasks, they cannot be called from command-line. Mixins can be\ncalled from both tasks and other mixins." + } + }, + "required": [ + "version" + ], + "description": "Gilbert workflow file.", + "$defs": { + "RecordString": { + "type": "object", + "properties": {}, + "additionalProperties": { + "type": "string" + } + }, + "RecordUnknown": { + "type": "object", + "properties": {}, + "additionalProperties": {} + }, + "ListInputDefinition": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "list", + "description": "Declares this input as a list value." + }, + "items": { + "anyOf": [ + { + "type": "string", + "const": "string" + }, + { + "type": "string", + "const": "int" + }, + { + "type": "string", + "const": "bool" + }, + { + "type": "string", + "const": "date" + }, + { + "type": "string", + "const": "duration" + }, + { + "type": "string", + "const": "float" + } + ], + "description": "Defines a type of element of a list.\nShould only be used when `type` is set to `list`.\n\nList item types must be scalar. Nested lists and dictionaries are not\nsupported." + } + }, + "required": [ + "type", + "items" + ], + "allOf": [ + { + "$ref": "#/$defs/InputDefinitionCommon" + } + ], + "description": "Input definition for list values." + }, + "ScalarInputDefinition": { + "type": "object", + "properties": { + "type": { + "anyOf": [ + { + "type": "string", + "const": "string" + }, + { + "type": "string", + "const": "int" + }, + { + "type": "string", + "const": "bool" + }, + { + "type": "string", + "const": "date" + }, + { + "type": "string", + "const": "duration" + }, + { + "type": "string", + "const": "float" + } + ], + "description": "Input value type.\n\nBesides standard scalar values, special values are parseable from\ncommand-line flags or string values. `duration` values are parsed with\nGo's time.ParseDuration. `date` values are parsed with the Go time layout\nfrom `dateFormat`, or RFC3339 when `dateFormat` is omitted." + }, + "dateFormat": { + "type": "string", + "description": "Date format used to parse a command-line flag value.\nHas effect only when `type` is `date`.\nDefaults to Go time.RFC3339 when omitted." + } + }, + "required": [ + "type" + ], + "allOf": [ + { + "$ref": "#/$defs/InputDefinitionCommon" + } + ], + "description": "Input definition for scalar values." + }, + "RecordTask": { + "type": "object", + "properties": {}, + "additionalProperties": { + "$ref": "#/$defs/Task" + } + }, + "RecordMixin": { + "type": "object", + "properties": {}, + "additionalProperties": { + "$ref": "#/$defs/Mixin" + } + }, + "InputDefinitionCommon": { + "type": "object", + "properties": { + "default": { + "description": "Fallback value used when input value is not specified.\nThis value may be an expression. After expression expansion, the value\nmust match the declared `type`." + }, + "optional": { + "type": "boolean", + "description": "Whether a value is not required.\nHas no effect when `default` is set.\n\nWhen `optional` is true and there is no default, the input resolves to the\ndeclared type's zero value. Boolean inputs are optional by default." + }, + "binding": { + "$ref": "#/$defs/InputBinding", + "description": "Controls how input value is parsed from command-line flags and environment variables.\nWorks only for tasks. Has no effect in mixins." + } + }, + "description": "Fields common to all input definitions." + }, + "Task": { + "type": "object", + "properties": { + "inputs": { + "type": "object", + "properties": {}, + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/ListInputDefinition" + }, + { + "$ref": "#/$defs/ScalarInputDefinition" + } + ] + }, + "description": "List of input parameters accepted by a task.\nInput values can come from explicit `with` values, command-line flags,\nenvironment variables, or defaults." + }, + "env": { + "$ref": "#/$defs/RecordUnknown", + "description": "Custom environment variables to use for a task." + }, + "steps": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/ActionJob" + }, + { + "$ref": "#/$defs/MixinJob" + }, + { + "$ref": "#/$defs/TaskJob" + } + ] + }, + "description": "Sequence of jobs that will be executed when task is started." + }, + "working-directory": { + "type": "string", + "description": "Override a working directory for a task." + } + }, + "required": [ + "steps" + ], + "description": "Task definition." + }, + "Mixin": { + "type": "object", + "properties": { + "inputs": { + "type": "object", + "properties": {}, + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/ListInputDefinition" + }, + { + "$ref": "#/$defs/ScalarInputDefinition" + } + ] + }, + "description": "List of input parameters accepted by mixin.\nUnlike tasks, mixins do not support command-line flags. Mixins can be\ncalled from tasks and from other mixins, and their inputs should be passed\nexplicitly via `with`." + }, + "env": { + "$ref": "#/$defs/RecordUnknown", + "description": "Custom environment variables to use for a mixin." + }, + "steps": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/ActionJob" + }, + { + "$ref": "#/$defs/MixinJob" + }, + { + "$ref": "#/$defs/TaskJob" + } + ] + }, + "description": "Sequence of jobs that will be executed when mixin is called." + }, + "working-directory": { + "type": "string", + "description": "Override a working directory for a mixin." + } + }, + "required": [ + "steps" + ], + "description": "Mixins allow a set of jobs to be reused as a block.\nMixin inputs are set only through `with` from a calling task or mixin;\ncommand-line flag and environment bindings are ignored for mixins." + }, + "InputBinding": { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "Environment variable name.\nIf input value and command-line flag are unspecified, this variable is used when present.\n\nTask input resolution order is: explicit `with` value, command-line flag,\nenvironment variable, then `default`. Bindings apply only to task inputs,\nnot mixin inputs." + }, + "flag": { + "type": "string", + "description": "Overrides the command-line flag name assigned to an input.\nBy default, each input gets a command-line flag with the same name as the input." + }, + "delimiter": { + "type": "string", + "description": "Character used to split a string into a list when parsing an environment\nvariable or command-line value.\n\nCurrent loader support is not implemented." + } + }, + "description": "Command-line and environment binding for an input value." + }, + "ActionJob": { + "type": "object", + "properties": { + "action": { + "type": "string", + "description": "Name of action to be executed.\nBuilt-in actions use `namespace/name` names such as `debug/echo` and\n`go/build`. Plugin actions use the namespace declared in `plugins`, for\nexample `docker/build`." + } + }, + "required": [ + "action" + ], + "allOf": [ + { + "$ref": "#/$defs/JobBase" + } + ], + "description": "Job that executes an action." + }, + "MixinJob": { + "type": "object", + "properties": { + "mixin": { + "type": "string", + "description": "Name of mixin to be executed.\nMixin inputs must be passed explicitly with `with`; command-line input\nbindings do not apply to mixin calls." + } + }, + "required": [ + "mixin" + ], + "allOf": [ + { + "$ref": "#/$defs/JobBase" + } + ], + "description": "Job that executes a mixin." + }, + "TaskJob": { + "type": "object", + "properties": { + "task": { + "type": "string", + "description": "Name of task to be executed.\nCalls another task from the current workflow, enabling nested task calls." + } + }, + "required": [ + "task" + ], + "allOf": [ + { + "$ref": "#/$defs/JobBase" + } + ], + "description": "Job that executes a task." + }, + "JobBase": { + "type": "object", + "properties": { + "async": { + "type": "boolean", + "description": "Whether to execute job concurrently.\nWhen true, the next job will start without waiting for the current one to finish." + }, + "delay": { + "type": "string", + "description": "Duration of time to wait before starting a job." + }, + "working-directory": { + "type": "string", + "description": "Path to a working directory where job will be executed.\nRelative paths resolve from the current workflow working directory." + }, + "timeout": { + "type": "string", + "description": "Job execution timeout duration." + }, + "continue-on-error": { + "type": "boolean", + "description": "Whether to allow job to fail." + }, + "if": { + "type": "string", + "description": "Whether to run a job.\nIf expression returns false, the job is skipped.\n\nThis field must be an expression. Static literal values are invalid." + }, + "env": { + "$ref": "#/$defs/RecordUnknown", + "description": "Custom environment variables to set for a job.\n\nThis block is literal. Expressions are not evaluated in keys or values." + }, + "strategy": { + "$ref": "#/$defs/Strategy", + "description": "Matrix execution strategy.\nExpands this job into the Cartesian product of matrix values." + }, + "with": { + "$ref": "#/$defs/RecordUnknown", + "description": "Input parameters for action, mixin or task.\nMap keys are literal. Expressions are allowed recursively in values." + }, + "on": { + "type": "object", + "properties": {}, + "additionalProperties": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/ActionJob" + }, + { + "$ref": "#/$defs/MixinJob" + }, + { + "$ref": "#/$defs/TaskJob" + } + ] + } + }, + "description": "Defines a set of jobs to run when a certain hook is called.\nHooks are events produced by certain actions.\n\nThe runner also provides a built-in `error` hook for handling job\nfailures. Jobs inside hook lists receive the `event` expression context." + } + }, + "required": [ + "with" + ], + "description": "Contains fields common to all job types." + }, + "Strategy": { + "type": "object", + "properties": { + "max-parallel": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "description": "Maximum number of matrix jobs to run concurrently.\n\nDefault: 1." + }, + "matrix": { + "type": "object", + "properties": {}, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": {} + } + ] + }, + "description": "Matrix of different job configurations.\nVariables defined in a matrix become properties in the `matrix` expression context.\n\nEach key becomes available as `matrix.` during each matrix job run.\nValues may be literal arrays or expressions that evaluate to arrays.\nEmpty arrays are ignored with a warning." + }, + "exclude": { + "type": "array", + "items": { + "$ref": "#/$defs/RecordUnknown" + }, + "description": "List of combinations to exclude from job matrix.\nA rule matches only when all key/value pairs in the rule match the matrix\nrow. Exclude values and referenced matrix values must be scalar and\ncomparable." + } + }, + "required": [ + "matrix" + ], + "description": "Matrix execution strategy." + } + } +} diff --git a/docs/spec/schema.tsp b/docs/spec/schema.tsp new file mode 100644 index 00000000..1b41d9fd --- /dev/null +++ b/docs/spec/schema.tsp @@ -0,0 +1,360 @@ +import "@typespec/json-schema"; + +using JsonSchema; + +namespace Schemas; + +/** + * A YAML string parseable by Go's time.ParseDuration. + * + * Examples: `300ms`, `10s`, `5m`, `2h45m`, `1.5h`. + */ +alias DurationString = string; + +/** + * YAML string containing a Gilbert expression. + * + * Gilbert supports three expression forms: + * + * - Shell expression: `$(...)`. The body is executed by the system shell, + * has a fixed 30 second timeout, and runs in `project.workDir`. + * - Value expression: `${{ ... }}`. The body is evaluated by the Expr + * language. When this is the entire YAML value, it can return a typed value + * such as a boolean, number, string, list, or object. + * - Mixed expression: literal text combined with one or more shell or value + * expressions. Mixed expressions always produce a string. Value-expression + * results inside mixed expressions are cast to strings; objects and lists + * cannot be cast. + * + * The type parameter from schema.d.ts is documentation-only, so the JSON + * schema representation is a string. + */ +alias Expression = string; + +/** Any YAML value. */ +alias AnyValue = unknown; + +/** YAML object with string keys and arbitrary YAML values. */ +alias AnyRecord = Record; + +/** + * Contains fields common to all job types. + */ +model JobBase { + /** + * Whether to execute job concurrently. + * When true, the next job will start without waiting for the current one to finish. + */ + async?: boolean; + + /** Duration of time to wait before starting a job. */ + delay?: DurationString; + + /** + * Path to a working directory where job will be executed. + * Relative paths resolve from the current workflow working directory. + */ + `working-directory`?: string; + + /** Job execution timeout duration. */ + timeout?: DurationString; + + /** Whether to allow job to fail. */ + `continue-on-error`?: boolean; + + /** + * Whether to run a job. + * If expression returns false, the job is skipped. + * + * This field must be an expression. Static literal values are invalid. + */ + `if`?: Expression; + + /** + * Custom environment variables to set for a job. + * + * This block is literal. Expressions are not evaluated in keys or values. + */ + env?: AnyRecord; + + /** + * Matrix execution strategy. + * Expands this job into the Cartesian product of matrix values. + */ + strategy?: Strategy; + + /** + * Input parameters for action, mixin or task. + * Map keys are literal. Expressions are allowed recursively in values. + */ + with: AnyRecord; + + /** + * Defines a set of jobs to run when a certain hook is called. + * Hooks are events produced by certain actions. + * + * The runner also provides a built-in `error` hook for handling job + * failures. Jobs inside hook lists receive the `event` expression context. + */ + on?: Record; +} + +/** Matrix execution strategy. */ +model Strategy { + /** + * Maximum number of matrix jobs to run concurrently. + * + * Default: 1. + */ + `max-parallel`?: int32; + + /** + * Matrix of different job configurations. + * Variables defined in a matrix become properties in the `matrix` expression context. + * + * Each key becomes available as `matrix.` during each matrix job run. + * Values may be literal arrays or expressions that evaluate to arrays. + * Empty arrays are ignored with a warning. + */ + matrix: Record; + + /** + * List of combinations to exclude from job matrix. + * A rule matches only when all key/value pairs in the rule match the matrix + * row. Exclude values and referenced matrix values must be scalar and + * comparable. + */ + exclude?: AnyRecord[]; +} + +/** Matrix values may be a literal array or an expression returning an array. */ +alias MatrixValues = Expression | AnyValue[]; + +/** Job that executes an action. */ +model ActionJob extends JobBase { + /** + * Name of action to be executed. + * Built-in actions use `namespace/name` names such as `debug/echo` and + * `go/build`. Plugin actions use the namespace declared in `plugins`, for + * example `docker/build`. + */ + action: string; +} + +/** Job that executes a mixin. */ +model MixinJob extends JobBase { + /** + * Name of mixin to be executed. + * Mixin inputs must be passed explicitly with `with`; command-line input + * bindings do not apply to mixin calls. + */ + mixin: string; +} + +/** Job that executes a task. */ +model TaskJob extends JobBase { + /** + * Name of task to be executed. + * Calls another task from the current workflow, enabling nested task calls. + */ + task: string; +} + +/** + * Job is a single execution unit of a task or mixin. + * Job can start an action, run a mixin, or call a different task. + */ +alias Job = ActionJob | MixinJob | TaskJob; + +/** Input value types supported by task and mixin inputs. */ +alias InputValueType = "string" | "int" | "bool" | "date" | "duration" | "float"; + +/** + * List item value types supported by list inputs. + * Nested complex types are not supported: a list item can be scalar, but not + * another list or dictionary. + */ +alias ListItemValueType = InputValueType; + +/** Command-line and environment binding for an input value. */ +model InputBinding { + /** + * Environment variable name. + * If input value and command-line flag are unspecified, this variable is used when present. + * + * Task input resolution order is: explicit `with` value, command-line flag, + * environment variable, then `default`. Bindings apply only to task inputs, + * not mixin inputs. + */ + env?: string; + + /** + * Overrides the command-line flag name assigned to an input. + * By default, each input gets a command-line flag with the same name as the input. + */ + flag?: string; + + /** + * Character used to split a string into a list when parsing an environment + * variable or command-line value. + * + * Current loader support is not implemented. + */ + delimiter?: string; +} + +/** Fields common to all input definitions. */ +model InputDefinitionCommon { + /** + * Fallback value used when input value is not specified. + * This value may be an expression. After expression expansion, the value + * must match the declared `type`. + */ + default?: AnyValue; + + /** + * Whether a value is not required. + * Has no effect when `default` is set. + * + * When `optional` is true and there is no default, the input resolves to the + * declared type's zero value. Boolean inputs are optional by default. + */ + optional?: boolean; + + /** + * Controls how input value is parsed from command-line flags and environment variables. + * Works only for tasks. Has no effect in mixins. + */ + binding?: InputBinding; +} + +/** Input definition for list values. */ +model ListInputDefinition extends InputDefinitionCommon { + /** Declares this input as a list value. */ + type: "list"; + + /** + * Defines a type of element of a list. + * Should only be used when `type` is set to `list`. + * + * List item types must be scalar. Nested lists and dictionaries are not + * supported. + */ + items: ListItemValueType; +} + +/** Input definition for scalar values. */ +model ScalarInputDefinition extends InputDefinitionCommon { + /** + * Input value type. + * + * Besides standard scalar values, special values are parseable from + * command-line flags or string values. `duration` values are parsed with + * Go's time.ParseDuration. `date` values are parsed with the Go time layout + * from `dateFormat`, or RFC3339 when `dateFormat` is omitted. + */ + type: InputValueType; + + /** + * Date format used to parse a command-line flag value. + * Has effect only when `type` is `date`. + * Defaults to Go time.RFC3339 when omitted. + */ + dateFormat?: string; +} + +/** Defines task or mixin input parameter. */ +alias InputDefinition = ListInputDefinition | ScalarInputDefinition; + +/** + * Mixins allow a set of jobs to be reused as a block. + * Mixin inputs are set only through `with` from a calling task or mixin; + * command-line flag and environment bindings are ignored for mixins. + */ +model Mixin { + /** + * List of input parameters accepted by mixin. + * Unlike tasks, mixins do not support command-line flags. Mixins can be + * called from tasks and from other mixins, and their inputs should be passed + * explicitly via `with`. + */ + inputs?: Record; + + /** Custom environment variables to use for a mixin. */ + env?: AnyRecord; + + /** Sequence of jobs that will be executed when mixin is called. */ + steps: Job[]; + + /** Override a working directory for a mixin. */ + `working-directory`?: string; +} + +/** Task definition. */ +model Task { + /** + * List of input parameters accepted by a task. + * Input values can come from explicit `with` values, command-line flags, + * environment variables, or defaults. + */ + inputs?: Record; + + /** Custom environment variables to use for a task. */ + env?: AnyRecord; + + /** Sequence of jobs that will be executed when task is started. */ + steps: Job[]; + + /** Override a working directory for a task. */ + `working-directory`?: string; +} + +/** Gilbert workflow file. */ +@jsonSchema +@id("gilbert.schema.json") +model WorkflowFile { + /** Workflow file version. `2` is the only supported value; other values are invalid. */ + version: 2; + + /** + * List of other workflow files to include and merge. + * Paths resolve relative to the including file. A file cannot include + * itself. Duplicate task, mixin, and input names across included files are + * rejected. + */ + include?: string[]; + + /** + * Set of plugins to import. + * Key is a namespace and value is import URL. + * + * The namespace becomes the action prefix. For example, a plugin declared as + * `docker:` exposes actions such as `docker/build` and `docker/run`. + */ + plugins?: Record; + + /** + * Predefined variables available to expressions as `${{ consts.* }}`. + * Expressions are not expanded inside this block. Values are literal YAML + * scalars. + */ + `const`?: AnyRecord; + + /** Global input parameters. */ + inputs?: Record; + + /** + * Map of task name and its definition. + * Tasks are command-line entry points and can be run with + * `gilbert run `. Comments immediately above a task key are collected + * as the task description for list/help output. + */ + tasks?: Record; + + /** + * Mixins are reusable pieces of pipeline which can accept inputs. + * Unlike tasks, they cannot be called from command-line. Mixins can be + * called from both tasks and other mixins. + */ + mixins?: Record; +} diff --git a/docs/spec/src/eval_context.d.ts b/docs/spec/src/eval_context.d.ts new file mode 100644 index 00000000..88ab400c --- /dev/null +++ b/docs/spec/src/eval_context.d.ts @@ -0,0 +1,71 @@ +/** + * @schema + * This file describes a schema of available variables in Expr lang expressions using TypeScript type syntax. + * It is NOT part of the application codebase (which is in Go). + * Do not generate TypeScript code from this file. + * + * See: + * - @./schema.d.ts - workflow file (`gilbert.yaml`) schema. + * - @./expressions.md - expression language documentation. + */ + +/** + * Holds global scope available in `${{...}}` expressions. + * + * Acts like `globalThis`/`window` in JavaScript. + */ +interface Scope { + /** + * Carries values provided to task/mixin inputs. + * + * @see [JobBase.on] in workflow file schema. + */ + inputs?: Record + + /** + * Holds constants defined in `const` section of workflow file. + * + * @see [WorkflowFile.const] + */ + consts?: Record + + project: { + /** + * Current job working directory. + */ + workDir: string + + /** + * Directory where workflow file is located. + */ + workspaceDir: string + + /** + * Path to workflow file. + */ + workflowFile: string + } + + /** + * System environment variables. + */ + env: Record + + /** + * Matrix values populated to a matrix strategy job. + * + * Only available if job has `.strategy.matrix` defined. + * + * @see [JobBase.strategy.matrix] + */ + matrix?: Record + + /** + * Metadata provided with a signal, fired by action. + * + * Only available if job is defined in `.on` block. + * + * @see [JobBase.on] + */ + event?: Record +} diff --git a/docs/spec/src/expressions.md b/docs/spec/src/expressions.md new file mode 100644 index 00000000..e9456b7b --- /dev/null +++ b/docs/spec/src/expressions.md @@ -0,0 +1,94 @@ +--- +agent_note: See eval_context.d.ts for expression context variables. +--- + +# Gilbert Expression Syntax + +The Gilbert task runner supports expanding expressions. +Expressions are defined as YAML string values. + +## Expression Types + +### Shell call expression + +Substitutes a `$(...)` section in a string with a result of a shell command inside of it. + +Example: + +- **Expression:** `"$(printf 2+2=%d 4)"` +- **Result:** `"4"` + +### Value Expression + +TODO: find a better name for this type of expression. + +Runs an [Expr language][expr] expression inside `${{...}}` and returns its value. + +[expr]: https://expr-lang.org/ + +#### Examples + +##### Basic Operations + +- **Expression:** `"${{ 2+2*3 }}"` +- **Result:** `8` + +> [!NOTE] +> Unlike shell expressions, this example returns a numeric value. + +### Mixed Expression + +A string which contains both shell, value expression and string literals. + +Returns a string. + +- **Expression:** `"Home of $(whoami) is ${{env.HOME}}"` +- **Result:** `"Home of Alice is /Users/Alice"` + +##### Accessing Context Variables + +Assume given a following workflow file: + +```yaml +const: + is_prod: true +``` + +- **Expression:** `"${{consts.is_prod}}"` +- **Result:** `true` + +> [!NOTE] +> See [Available Context Variables](#available-context-variables) for a list of variables available inside expressions. + +### Mixed Expression + +Both shell and language expresssions can be mixed together. +This type of expression always return string. + +Example: + +- **Expression:** `"$(whoami)'s home dir is ${{env.HOME}}"` +- **Result:** `"root's home dir is /root"` + +## Available Context Variables + +List of variables available in language expressions: + +| Property | Description | Example | +| --------- | ----------------------------------------------------------------------------------------------- | ---------------------- | +| `project` | see [docs below](#project) | `${{project.workDir}}` | +| `consts` | Holds values of constants declared in `const` section of `gilbert.yaml` | `${{consts.MY_CONST}}` | +| `env` | Shell environment variables | `${{env.HOME}}` | +| `inputs` | Holds values for input parameters. | `${{inputs.foobar}}` | +| `matrix` | Matrix values given passed to a job. See `job.strategy.matrix` of `gilbert.yaml` | `${{matrix.goos}}` | +| `event` | Holds event args when action fires on signal. Available only in jobs defined inside `job.on.*`. | `${{event.error}}` | + +### `project` + +Contains current working directory and a location from where task runner was started. + +| Property | Description | +| -------------- | --------------------------------------------------- | +| `workDir` | Path to a current working directory. | +| `workspaceDir` | Path to a directory where `gilbert.yaml` is located | +| `workflowFile` | Path to a current `gilbert.yaml`. | diff --git a/docs/spec/src/gilbert.yml b/docs/spec/src/gilbert.yml new file mode 100644 index 00000000..5affab5f --- /dev/null +++ b/docs/spec/src/gilbert.yml @@ -0,0 +1,172 @@ +# This is an example file that accompanies the @./schema.d.ts file. +version: 2 + +include: + - include.yml + +plugins: + githubMod: github://go-gilbert/example-plugin@v1.0.0 + httpMod: http://example.com/plugin.zip + localMod: ./my-plugin + +const: + foo: bar + +inputs: + # Is build a production release or not + release: + default: false + type: bool + + # Examples of other inputs of types + timeout: + type: duration + default: 10s + binding: + env: JOB_TIMEOUT + timestamp: + type: date + optional: true + dateFormat: "2006-01-02 15:04:05" + items: + type: list + items: + type: int + +mixins: + hello: + inputs: + name: + type: string + greeting: + type: string + default: Hello + steps: + - action: debug/echo + with: + message: "${{inputs.greeting}}, ${{inputs.name}}!" + - action: debug/echo + delay: 300ms + with: + message: "Goodbye, ${{inputs.name}}" + +tasks: + test:subtask: + inputs: + message: + type: string + steps: + - task: test:subtask:child + with: + msg: "subtasks with message: ${{inputs.message}}" + + test:subtask:child: + inputs: + msg: + type: string + steps: + - action: debug/echo + with: + message: "${{inputs.msg}}" + + test:mixin: + steps: + - mixin: hello + working-directory: .. + with: + name: Bob + greeting: Howdy + - action: debug/echo + with: + message: "This is the end, ${{inputs.name}}" + + # Task to debug task runner + test: + inputs: + # Test message content + message: + type: string + + # Signal name + signal: + type: string + default: mysignal + + steps: + - action: debug/signal + with: + signal: ${{inputs.signal}} + on: + mysignal: + - action: debug/echo + with: + message: "message: ${{inputs.message}}, data: ${{toJSON(event)}}" + error: + - action: debug/echo + with: + message: Error happened + - action: debug/echo + with: + message: next + + # Build builds application + build: + inputs: + # App version + version: + type: string + default: $(git describe --tags --abbrev=0) + steps: + - action: go/build + if: ${{inputs.version != "invalid"}} + timeout: 30s + continue-on-error: false + env: + FOO: bar + strategy: + max-parallel: 2 + matrix: + os: [windows, linux, darwin] + arch: [amd64, 386, arm64] + exclude: + - os: darwin + arch: 386 + - arch: amd64 + os: darwin + with: + package: . + os: ${{matrix.os}} + arch: ${{matrix.arch}} + cgo: false + buildvcs: false + tags: + - customtag + - '${{inputs.release ? "release" : "debug"}}' + package_vars: + "github.com/go-gilbert/gilbert/internal/legacy/build.Version": "${{inputs.version}}" + "github.com/go-gilbert/gilbert/internal/legacy/build.Channel": "stable" + + # Starts dev server. + # + # Detailed description comes here. + start: + inputs: + configFile: + type: string + default: "dev.env" + steps: + - action: fs/watch + delay: 1s + async: true + with: + path: "./..." + debounceTime: 100ms + ignore: + - "*.log" + on: + change: + - action: go/run + async: true + with: + package: . + args: "-cfg=${{ inputs.configFile }}" diff --git a/docs/spec/src/schema.d.ts b/docs/spec/src/schema.d.ts new file mode 100644 index 00000000..b79e880b --- /dev/null +++ b/docs/spec/src/schema.d.ts @@ -0,0 +1,446 @@ +/** + * @schema + * This file is a YAML schema description using TypeScript type syntax. + * It is NOT part of the application codebase (which is in Go). + * Do not generate TypeScript code from this file. + * + * Documentation conventions: + * + * - See @./expressions.md for expression language documentation. + * - "@const" annotation states that property should be a scalar value and expressions are not supported. + * - Properties that only allow expressions, use `Expression` type, where `T` is a return type of an expression. + */ + +/** + * A Yaml string type which a value parseable with Go's time.ParseDuration. + */ +type DurationString = string + +/** + * YAML string with expression, execution of which returns a type T. + * The `T` type carries a purely documentation role. + * + * Expression syntax is described in @./expressions.md + * + * Although most values can be a type of an expression string, + * this type states that a value should be an expression and cannot be a static value. + */ +type Expression = string + +interface JobAction { + /** + * Name of action to be executed. + */ + action: string +} + +interface JobMixin { + /** + * Name of mixin to be executed. + */ + mixin: string +} + +interface JobTask { + /** + * Name of task to be executed. + */ + mixin: string +} + +/** + * Contains fields common to all job types. + */ +interface JobBase { + /** + * Whether to execute job concurrently. + * When true, a next job will start, without waiting current one to finish. + */ + async?: boolean + + /** + * Duration of time to wait before starting a job. + */ + delay?: DurationString + + /** + * Path to a working directory where job will be executed. + */ + ["working-directory"]?: string + + /** + * Job execution timeout duration + */ + timeout?: DurationString + + /** + * Whether to allow job to fail. + */ + ["continue-on-error"]?: boolean + + /** + * Whether to run a job. + * + * If expression returns false - job is skipped. + */ + if?: Expression; + + /** + * Custom environment variables to set for a job. + * + * Example: + * + * ```yaml + * env: + * FOO: "bar" + * key: "${{inputs.myinput}}" + * ``` + * + * @const + */ + env?: Record + + /** + * Enables matrix strategy. + * Used to define variables whose combinations create separate job runs. + * For example, node: [18, 20] and os: [ubuntu-latest, macos-latest] creates four runs. + * + * NOTE: "@const" annotation means that value cannot be an expression and should be a literal YAML value. + */ + strategy?: { + /** + * Maximum number of matrix jobs to run concurrently. + * + * @const + * @default 1 + */ + ["max-parallel"]?: number + + /** + * Matrix of different job configurations. + * The variables defined in a matrix, become properties in `matrix` context. + * + * Example: + * + * ```yaml + * matrix: + * os: [darwin, linux, windows] + * arch: [amd64, arm64] + * ``` + * + * Matrix will populate `${{matrix.os}}` and `${{matrix.arch}}` properties to every job execution. + * + * @const + */ + matrix: Record | any[]> + + /** + * List of combinations to exclude from job matrix. + * + * @const + */ + exclude?: Record[] + } + + /** + * Input parameters for action, mixin or task. + * + * Tasks and mixins declare expected parameters in "inputs" block. + */ + with: Record + + /** + * Defines a set of jobs to run when certain hook is called. + * Hooks are events produced by certain actions. + * + * For example, the `fs/watch` action has `changed` hook which is called when filesystem contents are changed. + * + * Besides that, runner itself has a builtin `error` hook to handle job execution failures (e.g. resource cleanup). + */ + on?: Record +} + +type JobType = JobAction | JobMixin | JobTask + +/** + * Job is a single execution unit of a task or mixin. + * Job can start an action, run mixin or call a different task. + * + * TODO: improve description. + */ +type Job = JobType & JobBase + +interface ListInputDefinition { + /** + * @const + */ + type: 'list' + + /** + * Defines a type of element of a list. + * + * Should only be used when `type` is set to `list`. + * + * @const + */ + items: 'string' | 'int' | 'bool' | 'date' | 'duration' | 'float' | 'list' +} + +interface ScalarInputDefinition { + /** + * Input value type. + * + * Besides standard scalar values, it supports special types that are parceable from command-line flag or string value: + * - `duration`: time duration is nanoseconds. String value is parsed using Go's `time.ParseDuration` function. + * - `date`: Date and time. Parsed from a format defined in `dateFormat` field. + * + * @const + */ + type: 'string' | 'int' | 'bool' | 'date' | 'duration' | 'float' + items: never + + /** + * Date format used to parse a command-line flag value. + * + * Has effect only when `type` is `date`. + * + * @const + */ + dateFormat?: string +} + +interface InputDefinitionCommon { + /** + * Fallback value used when input value not specified. + * + * Value type should correspond to `type`. + * Note: use `optional` to use an empty value by default. + */ + default?: Expression | any + + /** + * Whether a value is not required. + * + * When enabled and input value is unspecified - sets an empty value for input. + * + * Has no effect when `default` is set. + * + * @const + */ + optional?: boolean + + /** + * Binding property controls how input value is parsed from command-line + * flags and environment variables. + * + * Works only for tasks. Has no effect in mixins. + * + * @const + */ + binding?: { + /** + * Name of environment variable. + * + * If input value is unspecified and command-line isn't present, + * value of a given environment variable will be used (if present). + * + * @const + */ + env?: string + + /** + * Overrides a name of command-line flag assigned to input. + * + * By default, each input gets a command-line flag with the same name as an input. + * + * @const + */ + flag?: string + + /** + * Character to be used to split a string into a list. + * + * Used when parsing a value from environment variable or command-line. + * + * Has no effect if input type is not `list`. + * + * @const + */ + delimiter?: string + } +} + +/** + * Defines task or mixin input parameter. + * Parameters are mapped to command-line flags. + * + * Input parameter description can be documented with a comment block below input block. + */ +type InputDefinition = InputDefinitionCommon & (ListInputDefinition | ScalarInputDefinition) + +/** + * Mixins allow to decople a set of jobs into a reusable block. + */ +interface Mixin { + /** + * List of input parameters accepted by mixin. + * + * NOTE: unlike tasks, mixins don't support using command-line flags. + * Input parameters for mixins should be explicitly passed via `with:` parameter in a job. + * + * @const + */ + inputs?: Record + + /** + * Custom environment variables to use for a mixin. + * + * Example: + * + * ```yaml + * env: + * FOO: "bar" + * key: "${{inputs.myinput}}" + * ``` + * + * @const + */ + env?: Record + + /** + * Sequence of jobs that will be executed when mixin is called. + * + * @const + */ + steps: Job[] + + /** + * Override a working directory for a mixin. + * @const + */ + ["working-directory"]?: string +} + +interface Task { + /** + * List of input parameters accepted by a task. + * + * Input values can be passed to a task via command-line flags. + * + * @const + */ + inputs?: Record + + /** + * Custom environment variables to use for a task. + * + * Example: + * + * ```yaml + * env: + * FOO: "bar" + * key: "${{inputs.myinput}}" + * ``` + * + * @const + */ + env?: Record + + /** + * Sequence of jobs that will be executed when task is started. + * + * @const + */ + steps: Job[] + + /** + * Override a working directory for a task. + * @const + */ + ["working-directory"]?: string +} + +interface WorkflowFile { + /** + * Workflow file version. Should be `2`. + * @const + */ + version: 2 + + /** + * List of other workflow files to include and merge. + * + * @const + */ + include?: string[] + + /** + * Set of plugins to import. + * + * Key is a namespace and value is import URL. + * + * For example, plugin with a following import, will expose its actions via `mydocker/` prefix: + * + * ```yaml + * plugins: + * mydocker: github://go-gilbert/gilbert-contrib/docker + * tasks: + * foo: + * steps: + * - action: mydocker/run + * with: + * ... + * ``` + * + * @const + */ + plugins?: Record + + /** + * List of predefined variables to be used in expressions. + * + * Values defined in this block are available as `${{consts.*}}` + * + * Value should not be an expression. + * + * @const + */ + const?: Record + + /** + * Global input parameters. + * + * @see [Task.inputs] + * @const + */ + inputs?: Record + + /** + * Map of task name and its definition. + * + * Task description can be documented via a comment block below its name: + * + * ```yaml + * tasks: + * # Build the project + * build: + * steps: + * ... + * ``` + * + * Documented task description will be present in `gilbert list` output. + * + * Task can be called by name using `gilbert run `. + * + * Use `gilbert list` to display a list of available tasks. + * Use `gilbert run --help` see task description and its parameters. + */ + tasks?: Record + + /** + * Mixins are reusable pieces of pipeline which can accept inputs. + * + * Unlike tasks, they cannot be called from command-line. + */ + mixins?: Record +} diff --git a/docs/spec/src/types.md b/docs/spec/src/types.md new file mode 100644 index 00000000..a2f36d13 --- /dev/null +++ b/docs/spec/src/types.md @@ -0,0 +1,24 @@ +# Workflow File + +Workflow file is a YAML file named `gilbert.yaml` which contains definitions of executable tasks. + +## Schema + +See: @./schema.d.ts + +## Input Types + +Gilbert introduces a set of types available for inputs. Types above are convertable from/to YAML: + +| Name | Description | YAML equivalent | +| ---------- | --------------------------------------------------------------------------------------------- | -------------------------- | +| `string` | String | Strings, including heredoc | +| `int` | signed 64-bit integer | number | +| `float` | floating point number | number | +| `date` | Date and time | string | +| `duration` | Time diration in nanoseconds. String values are parsed using `time.ParseDuration` Go function | string | +| `list` | Array of elements | List block | + +## Expressions + +See @./expressions.md diff --git a/docs/spec/workflow.md b/docs/spec/workflow.md new file mode 100644 index 00000000..49973567 --- /dev/null +++ b/docs/spec/workflow.md @@ -0,0 +1,490 @@ +# Gilbert Workflow File Specification + +This document specifies the Gilbert workflow file format, the input schema model, and the expression language available inside workflow values. + +The workflow file is a YAML document named `gilbert.yaml` or `gilbert.yml`. It declares executable tasks, reusable mixins, plugins, constants, and input definitions. + +Reference material: + +- `src/schema.d.ts` describes the YAML shape in TypeScript syntax for documentation. +- `src/eval_context.d.ts` describes variables available to expression evaluation. +- `src/expressions.md` and `src/types.md` describe expression and input-type behavior. +- `src/gilbert.yml` is an example workflow. + +## Document Shape + +```yaml +version: "2" +include: [] +plugins: {} +const: {} +inputs: {} +mixins: {} +tasks: {} +``` + +All top-level sections except `version` are optional unless a task needs them. Unknown fields are invalid when the loader validates a structured block. + +| Field | Required | Type | Expressions | Description | +| --- | --- | --- | --- | --- | +| `version` | Yes | string or number | No | Workflow format version. The supported value is `2`. | +| `include` | | list of strings | No | Other workflow files to load and merge into the current workflow. Paths are resolved relative to the file that declares them. | +| `plugins` | | map of string to string | No | Plugin imports. The map key is the action namespace exposed by the plugin. | +| `const` | | map of scalar values | No | Static values exposed to expressions as `consts.*`. | +| `inputs` | | map of [input definitions](#input-definition) | Per definition | Global input definitions. | +| `mixins` | | map of [mixin definitions](#tasks-and-mixins) | Per mixin field | Reusable job groups that can be called from tasks or other mixins. | +| `tasks` | | map of [task definitions](#tasks-and-mixins) | Per task field | Runnable entry points. A task can be executed with `gilbert run `. | + +## Includes + +`include` is a list of YAML file paths: + +```yaml +include: + - ./common.yml +``` + +Each path must point to a file, cannot point to the current file, and is resolved relative to the including file. Included workflows are merged into the current workflow. Duplicate definitions are rejected for sections that require unique names. + +## Plugins + +`plugins` maps a namespace to an import URL: + +```yaml +plugins: + docker: github://go-gilbert/gilbert-contrib/docker + local: ./plugins/local +``` + +Actions exported by the plugin are referenced with the namespace prefix: + +```yaml +tasks: + image: + steps: + - action: docker/build + with: + context: . +``` + +## Constants + +`const` defines scalar values available to `${{ ... }}` expressions through the `consts` context: + +```yaml +const: + release_channel: stable + publish: true +``` + +Constants are literal YAML scalars. Expressions are not expanded inside `const`. + +## Inputs + +Inputs declare values accepted by the workflow, tasks, and mixins. + +```yaml +inputs: + release: + type: bool + default: false + + timeout: + type: duration + default: 10s + binding: + env: JOB_TIMEOUT + flag: timeout +``` + +Task inputs can be set from command-line flags, environment variables, defaults, or explicit `with` values. Mixin inputs are passed explicitly from the calling job through `with`; command-line bindings do not apply to mixins. + +### Input Definition + +| Field | Required | Type | Expressions | Description | +| --- | --- | --- | --- | --- | +| `type` | Yes | [input type](#input-types) | No | One of [`string`](#string), [`int`](#int), [`bool`](#bool), [`float`](#float), [`date`](#date), [`duration`](#duration), or [`list`](#list). | +| `items` | when `type: list` | [type schema](#type-schema) | No | Invalid otherwise. Defines the list element type. | +| `dateFormat` | | string | No | Date parsing layout. Only valid for [`date`](#date). Defaults to Go `time.RFC3339`. | +| `default` | | value or [expression](#expression-language) | Yes | Fallback value when no value is provided. Must match the input type after expansion. | +| `optional` | | bool | No | Allows an omitted input to resolve to the type's zero value. Has no effect when `default` is set. Boolean inputs are optional by default. | +| `binding.env` | | string | No | Environment variable used when no explicit input or flag is provided. | +| `binding.flag` | | string | No | Command-line flag name. Defaults to the input name when omitted. | +| `binding.delimiter` | | string | No | Intended delimiter for parsing list values from flags or environment variables. Current loader support is not implemented. | + +### Input Types + +| Type | YAML representation | Runtime value | +| --- | --- | --- | +| [`string`](#string) | string | string | +| [`int`](#int) | number | signed 64-bit integer | +| [`float`](#float) | number | 64-bit floating-point number | +| [`bool`](#bool) | boolean | boolean | +| [`date`](#date) | string | parsed with `dateFormat` or RFC3339 | +| [`duration`](#duration) | string | parsed by Go `time.ParseDuration`, for example `300ms`, `10s`, `5m` | +| [`list`](#list) | YAML sequence or delimited input value | list of typed elements | + +List inputs use an `items` schema: + +```yaml +inputs: + ports: + type: list + items: + type: int +``` + +Nested complex item types are not supported. A list item type can be scalar, but not another list or dictionary. + +#### String + +`string` is text. YAML strings may be plain, quoted, or block scalars. + +```yaml +name: + type: string + default: Gilbert +``` + +#### Int + +`int` is a signed 64-bit integer. + +```yaml +retries: + type: int + default: 3 +``` + +#### Float + +`float` is a 64-bit floating-point number. + +```yaml +threshold: + type: float + default: 0.75 +``` + +#### Bool + +`bool` is a boolean value. Boolean inputs are optional by default. + +```yaml +release: + type: bool + default: false +``` + +#### Date + +`date` is a date/time value parsed from a string. By default, values use Go `time.RFC3339`. A custom Go time layout can be set with `dateFormat`. + +```yaml +publishedAt: + type: date + dateFormat: "2006-01-02 15:04:05" +``` + +#### Duration + +`duration` is a time duration parsed from a string with Go `time.ParseDuration`. Supported examples include `300ms`, `10s`, `5m`, `2h45m`, and `1.5h`. + +Duration values are also used by workflow fields described as a [duration string](#duration), such as job `delay` and `timeout`. + +```yaml +timeout: + type: duration + default: 10s +``` + +#### List + +`list` is an array of typed elements. List inputs must declare an `items` [type schema](#type-schema). + +```yaml +ports: + type: list + items: + type: int +``` + +### Type Schema + +A type schema is the object form used by `items` to describe a list element: + +```yaml +items: + type: int +``` + +| Field | Required | Type | Expressions | Description | +| --- | --- | --- | --- | --- | +| `type` | Yes | [input type](#input-types) | No | Element type. Complex nested types such as [`list`](#list) are not supported. | +| `dateFormat` | | string | No | Date parsing layout. Only valid when `type` is [`date`](#date). | + +## Tasks And Mixins + +Tasks and mixins share the same job-group shape: + +```yaml +tasks: + build: + inputs: {} + env: {} + working-directory: . + steps: [] + +mixins: + hello: + inputs: {} + env: {} + working-directory: . + steps: [] +``` + +| Field | Required | Type | Expressions | Description | +| --- | --- | --- | --- | --- | +| `inputs` | | map of [input definitions](#input-definition) | Per definition | Values accepted by this task or mixin. | +| `env` | | map | No | Environment values scoped to the task or mixin. | +| `working-directory` | | string | No | Working directory for jobs in the group. Relative paths resolve from the current workflow working directory. | +| `steps` | Yes | list of [jobs](#jobs) | Per job field | Jobs executed in order unless a job is asynchronous. | + +Tasks are command-line entry points. Mixins are reusable job groups and cannot be run directly from the command line. + +Comments immediately above task, mixin, and input keys may be collected as documentation by tooling. + +## Jobs + +A job is one step in a task, mixin, or event hook. It must target exactly one executable unit. + +```yaml +steps: + - action: debug/echo + with: + message: Hello + + - mixin: hello + with: + name: Bob + + - task: test:subtask + with: + message: nested task call +``` + +### Job Target + +Exactly one target field must be set. + +| Field | Required | Type | Expressions | Description | +| --- | --- | --- | --- | --- | +| `action` | one target | string | No | Action name. Built-in and plugin actions use namespace-style names such as `debug/echo` or `go/build`. | +| `mixin` | one target | string | No | Mixin name to execute. | +| `task` | one target | string | No | Task name to execute. | + +### Job Fields + +| Field | Required | Type | Expressions | Description | +| --- | --- | --- | --- | --- | +| `async` | | bool | No | When true, the runner starts the next job without waiting for this job to finish. | +| `delay` | | [duration string](#duration) | No | Time to wait before starting the job. | +| `working-directory` | | string | No | Working directory for this job. | +| `timeout` | | [duration string](#duration) | No | Maximum execution time for this job. | +| `continue-on-error` | | bool | No | When true, task execution continues after this job fails. | +| `if` | | [expression](#expression-language) | Required expression | Conditional expression. If it evaluates to false, the job is skipped. Static literal values are invalid here. | +| `env` | | map | No | Environment values scoped to this job. | +| `strategy` | | [strategy object](#matrix-strategy) | Per strategy field | Matrix execution strategy. | +| `with` | | map | Yes, recursively in values | Arguments passed to the action, mixin, or task. Map keys are literal. | +| `on` | | map of string to job list | Per hook job field | Event hooks fired by this job or by the runner. Hook names are literal. | + +## Matrix Strategy + +`strategy` expands one job into multiple job runs by computing the Cartesian product of matrix values. + +```yaml +steps: + - action: go/build + strategy: + max-parallel: 2 + matrix: + os: [windows, linux, darwin] + arch: [amd64, arm64] + exclude: + - os: darwin + arch: amd64 + with: + os: ${{ matrix.os }} + arch: ${{ matrix.arch }} +``` + +| Field | Required | Type | Expressions | Description | +| --- | --- | --- | --- | --- | +| `max-parallel` | | positive integer | No | Maximum number of matrix jobs to run concurrently. Defaults to `1`. | +| `matrix` | Yes | ordered map of string to list or expression returning list | Yes, in values | Each key becomes available under `matrix.` during each job run. Matrix keys are literal. | +| `exclude` | | list of scalar maps | No | Excludes matrix rows that match all specified key/value pairs in an exclude rule. | + +`matrix` values may be literal arrays or expressions that evaluate to arrays. Empty matrix arrays are ignored with a warning. `exclude` values and the referenced matrix values must be scalar and comparable. + +## Event Hooks + +Jobs may define `on` hooks. Each hook maps an event name to jobs that run when the event is emitted. + +```yaml +steps: + - action: debug/signal + with: + signal: changed + on: + changed: + - action: debug/echo + with: + message: "event: ${{ event.name }}" + error: + - action: debug/echo + with: + message: "job failed" +``` + +Actions may define their own hook names. The runner also supports an `error` hook for handling job failures. + +Jobs inside a hook receive the `event` expression context. + +## Expression Language + +Gilbert expands dynamic expressions in YAML string values where the schema allows lazy values. Expressions are parsed before execution and evaluated at runtime. + +There are three expression forms: + +| Form | Example | Result type | +| --- | --- | --- | +| Shell expression | `$(git describe --tags --abbrev=0)` | string | +| Value expression | `${{ inputs.release ? "release" : "debug" }}` | value returned by Expr | +| Mixed expression | `'some string, ${{ "expres" + "sion" }}, and $(echo shell)'` | string | + +When a string contains multiple literal or expression parts, it is a mixed expression. The parts are concatenated and the final value is a string: + +```yaml +message: "$(whoami)'s workspace is ${{ project.workspaceDir }}" +``` + +Values returned from `${{ ... }}` inside a mixed expression are cast to strings. String, number, and boolean values can be cast. Objects and lists cannot be cast and cause expression evaluation to fail. + +When a string contains exactly one value expression, the expression may return a non-string value: + +```yaml +cgo: ${{ inputs.release }} +``` + +Terminology note: this document uses "value expression" for `${{ ... }}` because the expression can return typed values when it is the whole YAML value. Other reasonable names are "Expr expression", "language expression", and "runtime expression"; "value expression" is the least awkward and describes the behavior most directly. + +### Shell Expressions + +Shell expressions use `$(...)`: + +```yaml +version: + type: string + default: $(git describe --tags --abbrev=0) +``` + +The body is executed by the system shell in the current expression scope. The working directory is `project.workDir`, and the command receives the current environment. Command evaluation has a fixed timeout of 30 seconds. + +Shell expressions can contain value expressions in their body: + +```yaml +value: $(echo "${{ inputs.name }}") +``` + +### Value Expressions + +Value expressions use `${{ ... }}` and are evaluated by the [Expr](https://expr-lang.org/) language: + +```yaml +if: ${{ inputs.version != "invalid" }} +``` + +Expr syntax supports operators, conditionals, function calls, selectors, indexing, and built-ins provided by Expr. See the Expr [language definition](https://expr-lang.org/docs/language-definition) and [functions](https://expr-lang.org/docs/functions) documentation for the syntax and standard functions available from Expr. + +Gilbert supplies the evaluation context described below. + +Nested Gilbert expressions are not allowed inside the body of a `${{ ... }}` expression. + +### Mixed Expressions + +Mixed expressions combine literal text with one or more shell or value expressions: + +```yaml +message: 'some string, ${{ "expres" + "sion" }}, and $(echo shell)' +``` + +Mixed expressions always produce a string. Shell expression results are already strings. Value expression results are converted to strings when possible; conversion fails for objects and lists. + +### Expression Context + +| Name | Type | Available when | Description | +| --- | --- | --- | --- | +| [`project`](#project-context) | object | always | Project paths and current working directory. | +| `env` | map of string to string | always | System environment variables. | +| `consts` | map | when constants exist | Values from the workflow `const` block. | +| `inputs` | map | inside task, mixin, job, and default evaluation scopes | Resolved input values. | +| `matrix` | map | inside matrix job runs | Current matrix row values. | +| `event` | map | inside jobs declared under `on` hooks | Event metadata emitted by an action or runner hook. | + +Input and constant lookups are inherited through nested scopes. A child scope can see values from its parent scope unless it overrides the same key. + +### Project Context + +`project` contains paths for the current workflow execution: + +| Name | Type | Description | +| --- | --- | --- | +| `project.workDir` | string | Current job working directory. | +| `project.workspaceDir` | string | Directory containing the workflow file. | +| `project.workflowFile` | string | Path to the current workflow file. | + +### Expression Placement + +Each schema table includes an `Expressions` column. The general rules are: + +- `No` means the value is literal and expressions are not evaluated. +- `Yes` means the field accepts shell expressions, value expressions, and mixed expressions. +- `Required` means the field must be an expression and cannot be a static literal. +- `Per ... field` means the object itself is structural, and expression support is determined by each child property. +- `recursively in values` means expressions are allowed in nested map and list values, but map keys remain literal. + +Common dynamic value positions include: + +- input `default` +- job `if` +- job `with` values +- matrix values +- nested arrays and maps inside dynamic values + +## JSON Schema Representation + +The workflow schema can be represented as JSON Schema for structural validation, but not as a complete replacement for the current TypeScript documentation and Go loader rules. + +A JSON Schema can model: + +- top-level sections and required `version` +- task, mixin, job, strategy, and input object shapes +- allowed scalar input type names +- `oneOf` rules for `action`, `mixin`, and `task` targets +- `if`/`then` rules such as requiring `items` when `type` is `list` +- `patternProperties` for name-to-definition maps +- duration and expression fields as strings + +A JSON Schema cannot fully model, without custom keywords or runtime validation: + +- parsing Go duration strings with exact `time.ParseDuration` behavior +- parsing dates with a user-provided Go layout in `dateFormat` +- validating Expr syntax inside `${{ ... }}` +- validating shell command expressions inside `$(...)` +- distinguishing all literal-only fields from expression-capable fields by runtime parser behavior +- enforcing include-file existence, recursive include checks, and merge conflicts +- validating action-specific `with` schemas from plugins +- checking matrix exclude comparability and whether exclude keys are present in the matrix +- collecting comments as task/input documentation + +Recommended approach: generate or maintain a JSON Schema for editor assistance and early structural diagnostics, while keeping the TypeScript schema and Go loader as the normative source for semantic validation. Use custom schema extensions such as `x-gilbert-expression`, `x-gilbert-const`, and `x-gilbert-duration` if tooling needs to preserve Gilbert-specific semantics. diff --git a/docs/test_jobs.yaml b/docs/test_jobs.yaml deleted file mode 100644 index 2e7ebdd7..00000000 --- a/docs/test_jobs.yaml +++ /dev/null @@ -1,48 +0,0 @@ -version: 1.0 - -#plugins: -# - github://github.com/go-gilbert/gilbert-plugin-example?version=v0.8.5 - -mixins: - async-test: - - action: shell - async: true - params: - command: 'foo' - -tasks: - test-subtask: - - task: '__foo' - vars: - message: 'Hello foo' - - task: 'test-plugins' - __foo: - - action: shell - params: - command: 'echo {{message}}' - test-plugins: - - action: 'example-plugin:hello-world' - params: - message: "i love vodka" - test-deadline-async: - - action: shell - deadline: 2000 - async: true - params: - command: 'sleep 4' - - action: shell - params: - command: 'ping -c 5 localhost' - win-test-async: - - action: shell - params: - command: ping google.com - test-async: - - mixin: async-test - - action: shell - params: - command: 'uname -a' - - action: shell - async: true - params: - command: 'ping -c 5 localhost' diff --git a/docs/testdata/gilbert.sh b/docs/testdata/gilbert.sh index 8854be2f..39e67fd2 100755 --- a/docs/testdata/gilbert.sh +++ b/docs/testdata/gilbert.sh @@ -1,3 +1,3 @@ #!/usr/bin/env sh set -e -go run ../../cmd/gilbert2 "$@" --log-level=debug +go run ../../cmd/gilbert "$@" --log-level=debug diff --git a/docs/todo.md b/docs/todo.md index f40ac544..967bf3bb 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -13,11 +13,13 @@ - input binding delimiter -**cobra** +**schema** -- Fix showing tasks list "Available Tasks" for "gilbert --help" +- wire `InputDefinition.binding.delimiter` to yamlloader +- support setting env +- support binding to file -**expr** +**actions** -- Support indents in DocumentInfo -- Consider cel-go as it might support dynamic envs: https://github.com/google/cel-go +- Add `node/npx`, will be used to run typespec schema gen. +- Add `fs/watch` and other. diff --git a/gilbert.yml b/gilbert.yml index 7f095f21..fea0c316 100644 --- a/gilbert.yml +++ b/gilbert.yml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=./docs/spec/gilbert.schema.json version: 2 const: diff --git a/internal/manifest/loader/yamlloader/inputs.go b/internal/manifest/loader/yamlloader/inputs.go index 7dfd8088..c172f3dd 100644 --- a/internal/manifest/loader/yamlloader/inputs.go +++ b/internal/manifest/loader/yamlloader/inputs.go @@ -7,11 +7,12 @@ import ( "strings" "time" + "github.com/goccy/go-yaml/ast" + "github.com/go-gilbert/gilbert/internal/manifest" "github.com/go-gilbert/gilbert/pkg/expr" "github.com/go-gilbert/gilbert/pkg/parsetypes" . "github.com/go-gilbert/gilbert/pkg/yamltree" - "github.com/goccy/go-yaml/ast" ) var listTypeSchema = Struct( @@ -159,6 +160,7 @@ var inputDefinitionSchema = Struct( Offset: offset, } }), + // TODO: add delimiter ), ), func(_ context.Context, dst *manifest.InputDefinition, val *manifest.InputBinding) error { diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..fe2d69e9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,953 @@ +{ + "name": "gilbert", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gilbert", + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@typespec/compiler": "^1.12.0", + "@typespec/json-schema": "^1.12.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.6.tgz", + "integrity": "sha512-I/INw4sHGlVZ/afZOckpLiDP9SmbMl1g/GCqeHjLw1Afw/0PlRs2tRFgTGWmdI0hoNuWZn3y2iHNmG1vyECyQQ==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.0.tgz", + "integrity": "sha512-1HJt+3fqxblp/GQjdntSyoSHYBc0e3CzXVgjFpKA6qFLd9FHBBqwN8Co0xYH6t2JVUZrtFwZ4bBiwptkiLxyOg==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.6", + "@inquirer/core": "^11.2.0", + "@inquirer/figures": "^2.0.6", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.0.tgz", + "integrity": "sha512-USpeB76eqK7yGricDlGAupxWlp4a59qpeZOoNWaxO/nJln7agpJveyNkQ1d5u8YXG6TOqxZtQpKPORQQDrdVsA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.0", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.0.tgz", + "integrity": "sha512-joR1YS2sI0us+9d0I8ViqFbrRLONO8CFTuyvBX4ZVBSch+VsZiugUABdrhBXXJR1VyEzvpz5SQCix3keETQ58g==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.6", + "@inquirer/figures": "^2.0.6", + "@inquirer/type": "^4.0.6", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^4.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.0.tgz", + "integrity": "sha512-/m+sgRmzSdK6HDtVnl3PmI6MnZC4O+LLezedoJcrX7mINhTjjb0hlC7aEDGZXkFTB4b5uQ0q59AhYTah88KbNg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.0", + "@inquirer/external-editor": "^3.0.1", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.0.tgz", + "integrity": "sha512-fR7g4BVnIcs+4NApF6C5byflNM/EULxSxsv/2Jvg+gmop0R6eBIPvZqE6RYnTy1tQTFnf9wyHkwNoQSZbofaGA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.0", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.1.tgz", + "integrity": "sha512-tam+Gwjsxg2sx3iUVPkAnhKT/yrk2rd2NAa7XJU/J8OYpU0ifXsnp12xlvzp/DCpWBXVv+vLQsqnpAWwUcWD5Q==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.6.tgz", + "integrity": "sha512-dsZgQtH2t5Q6ah3aPbZbeEZAxsD9qQu0DXf01AltuEfRTm+NoLN6+rLVbr+4edeEbNCp/wBNM6mALRWtsQpfkw==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.0.tgz", + "integrity": "sha512-sVZCz6P6e8tW5g2bSFel1oLpa6jK/u7BexFfrgTqR8syIdnHqy+iopnlSbYBZMsCK52chLjhGNBxt0eRqhsghw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.0", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.0.tgz", + "integrity": "sha512-VMXB/XejCbaSTf9Xucl7dqjzzsaGsrs6XwSYXPbGZ2QbSuq/Gz8XamhSi9ClRubNXZlGry9xVg1tKkJdTDgCtQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.0", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.0.tgz", + "integrity": "sha512-5tqRuKCDIUxdPxTI/CuLnh914kz+WMPmURHKnZgui9gk43ebudEsdu4EwSn1CPSi5R+17YpBG+ba/YqTnRAcJA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.6", + "@inquirer/core": "^11.2.0", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.5.0.tgz", + "integrity": "sha512-pLjXOnY4y3R1mgyHP3pXD/8eXejp+L/dde/0N2NLKgKfMstqhNZrpvs7Wkzbl9FYFQh10LRQ7QZwq+cz9rrhyw==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.0", + "@inquirer/confirm": "^6.1.0", + "@inquirer/editor": "^5.2.0", + "@inquirer/expand": "^5.1.0", + "@inquirer/input": "^5.1.0", + "@inquirer/number": "^4.1.0", + "@inquirer/password": "^5.1.0", + "@inquirer/rawlist": "^5.3.0", + "@inquirer/search": "^4.2.0", + "@inquirer/select": "^5.2.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.0.tgz", + "integrity": "sha512-p+vAeTAD+cGXjGleP1F5LXrX2ISxNDZm+lqeBpnJausNLSZskZZkcggwhomqP8Igx9oIjnoeOrw98xvdFvdm2w==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.0", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.0.tgz", + "integrity": "sha512-ByURoSGIaSl5O5Q0AmYmVmUsXbMUcBGNoA3FRL7TOyiA22IeFHymJKRkuILbOIlJwqnBk7AnPpseodyFUBzg+g==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.0", + "@inquirer/figures": "^2.0.6", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.0.tgz", + "integrity": "sha512-6IzkcmEbEXfgVbxZ2d1UyJFbCBoc6dTofulFmrYuomIp88HXiVqRbqbg4/mbfZhvnNo6xYmnYo2AEmDof6fQkg==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.6", + "@inquirer/core": "^11.2.0", + "@inquirer/figures": "^2.0.6", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.6.tgz", + "integrity": "sha512-J+9tdxOskuYuGjsvGaq00AamhDgjR7anhEW2dP4QdQpFCMPngCeC/bCYWQ5NsMWZRdsy53is7kAHb/+7cwDk2g==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@typespec/asset-emitter": { + "version": "0.79.1", + "resolved": "https://registry.npmjs.org/@typespec/asset-emitter/-/asset-emitter-0.79.1.tgz", + "integrity": "sha512-53s3GLu5BwNkl7Itr/OizfhymTV2u7k5/cwjUOAt03AUDfiKlwbsp+iCIsq1vccJuoDOiXOceJOfL8rAf4/9LQ==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.10.0" + } + }, + "node_modules/@typespec/compiler": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@typespec/compiler/-/compiler-1.12.0.tgz", + "integrity": "sha512-hKCkHEEDdCpXFyOU8ln+TzBBwonFMbkeUV0zIc+vBETyO8p/Upui3XvEyLOyB4CpKUReHzGeGm3gcFjNc73ygg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@inquirer/prompts": "^8.4.1", + "ajv": "^8.18.0", + "change-case": "^5.4.4", + "env-paths": "^4.0.0", + "is-unicode-supported": "^2.1.0", + "mustache": "^4.2.0", + "picocolors": "^1.1.1", + "prettier": "^3.8.1", + "semver": "^7.7.4", + "tar": "^7.5.13", + "temporal-polyfill": "^0.3.2", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-textdocument": "^1.0.12", + "yaml": "^2.8.3", + "yargs": "^18.0.0" + }, + "bin": { + "tsp": "cmd/tsp.js", + "tsp-server": "cmd/tsp-server.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@typespec/json-schema": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@typespec/json-schema/-/json-schema-1.12.0.tgz", + "integrity": "sha512-e/hxD6q0ThpCmIXOt4wseC30dv1lWnmQJaDhsn6MJySNVpcfTw9xVgj40Oeygc3TC1nO5X0cICtkOqDV/FMFAw==", + "license": "MIT", + "dependencies": { + "@typespec/asset-emitter": "^0.79.1", + "yaml": "^2.8.3" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.12.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "license": "MIT" + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-4.0.0.tgz", + "integrity": "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw==", + "license": "MIT", + "dependencies": { + "is-safe-filename": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/is-safe-filename": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-safe-filename/-/is-safe-filename-0.1.1.tgz", + "integrity": "sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/mute-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-4.0.0.tgz", + "integrity": "sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg==", + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/tar": { + "version": "7.5.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/temporal-polyfill": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/temporal-polyfill/-/temporal-polyfill-0.3.2.tgz", + "integrity": "sha512-TzHthD/heRK947GNiSu3Y5gSPpeUDH34+LESnfsq8bqpFhsB79HFBX8+Z834IVX68P3EUyRPZK5bL/1fh437Eg==", + "license": "MIT", + "dependencies": { + "temporal-spec": "0.3.1" + } + }, + "node_modules/temporal-spec": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/temporal-spec/-/temporal-spec-0.3.1.tgz", + "integrity": "sha512-B4TUhezh9knfSIMwt7RVggApDRJZo73uZdj8AacL2mZ8RP5KtLianh2MXxL06GN9ESYiIsiuoLQhgVfwe55Yhw==", + "license": "ISC" + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..db1d8b25 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "gilbert", + "version": "2.0.0", + "description": "", + "main": "index.js", + "directories": { + "doc": "docs" + }, + "scripts": {}, + "repository": { + "type": "git", + "url": "git+https://github.com/go-gilbert/gilbert.git" + }, + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/go-gilbert/gilbert/issues" + }, + "homepage": "https://github.com/go-gilbert/gilbert#readme", + "dependencies": { + "@typespec/compiler": "^1.12.0", + "@typespec/json-schema": "^1.12.0" + } +} diff --git a/tools/post-process-schema.mjs b/tools/post-process-schema.mjs new file mode 100644 index 00000000..d77af020 --- /dev/null +++ b/tools/post-process-schema.mjs @@ -0,0 +1,47 @@ +/** + * Post-processes a JSON Schema file emitted by TypeSpec's `@typespec/json-schema` emitter. + * Replaces all occurrences of `unevaluatedProperties` with `additionalProperties` to + * improve compatibility with YAML language servers. + */ + +import { readFileSync, writeFileSync } from "fs"; + +const [inputFile, outputFile] = process.argv.slice(2); + +if (!inputFile) { + console.error("Usage: node post-process-schema.mjs [outputFile]"); + process.exit(1); +} + +let data; +try { + data = JSON.parse(readFileSync(inputFile, "utf8")); +} catch (err) { + console.error(`Error: Failed to parse '${inputFile}' as JSON: ${err.message}`); + process.exit(1); +} + +function renameKeys(obj) { + if (Array.isArray(obj)) { + return obj.map(renameKeys); + } + if (obj !== null && typeof obj === "object") { + const result = {}; + for (const [key, value] of Object.entries(obj)) { + result[key === "unevaluatedProperties" ? "additionalProperties" : key] = + renameKeys(value); + } + return result; + } + return obj; +} + +const result = renameKeys(data); +const output = JSON.stringify(result, null, 2) + "\n"; + +if (outputFile) { + writeFileSync(outputFile, output); + console.log(`Patched: ${outputFile}`); +} else { + process.stdout.write(output); +}