@granular-software/sdk 0.4.12 → 0.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -10,6 +10,7 @@ var crypto = require('crypto');
10
10
  var process6 = require('process');
11
11
  var os = require('os');
12
12
  var tty = require('tty');
13
+ var metamodelPresetDefault = require('@granular-software/metamodel-preset-default');
13
14
 
14
15
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
15
16
 
@@ -3439,7 +3440,7 @@ var require_main = __commonJS({
3439
3440
  var os2 = __require("os");
3440
3441
  var crypto = __require("crypto");
3441
3442
  var packageJson = require_package();
3442
- var version = packageJson.version;
3443
+ var version2 = packageJson.version;
3443
3444
  var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;
3444
3445
  function parse(src) {
3445
3446
  const obj = {};
@@ -3488,13 +3489,13 @@ var require_main = __commonJS({
3488
3489
  return DotenvModule.parse(decrypted);
3489
3490
  }
3490
3491
  function _warn(message) {
3491
- console.log(`[dotenv@${version}][WARN] ${message}`);
3492
+ console.log(`[dotenv@${version2}][WARN] ${message}`);
3492
3493
  }
3493
3494
  function _debug(message) {
3494
- console.log(`[dotenv@${version}][DEBUG] ${message}`);
3495
+ console.log(`[dotenv@${version2}][DEBUG] ${message}`);
3495
3496
  }
3496
3497
  function _log(message) {
3497
- console.log(`[dotenv@${version}] ${message}`);
3498
+ console.log(`[dotenv@${version2}] ${message}`);
3498
3499
  }
3499
3500
  function _dotenvKey(options) {
3500
3501
  if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) {
@@ -6529,7 +6530,8 @@ async function main() {
6529
6530
 
6530
6531
  // Connect as one app user so the SDK can create an environment for the seed run.
6531
6532
  const env = await granular.connect({
6532
- sandbox: SANDBOX_ID,
6533
+ ontology: SANDBOX_ID,
6534
+ environment: 'dev',
6533
6535
  userId: '${template.seedUser.userId}',
6534
6536
  name: '${template.seedUser.name}',
6535
6537
  email: '${template.seedUser.email}',
@@ -6865,15 +6867,72 @@ var ApiClient = class {
6865
6867
  );
6866
6868
  return result.items;
6867
6869
  }
6870
+ async listVersions(sandboxId) {
6871
+ const result = await this.request(
6872
+ `/control/sandboxes/${sandboxId}/versions`
6873
+ );
6874
+ return result.items;
6875
+ }
6868
6876
  async getBuild(buildId) {
6869
6877
  return this.request(`/control/builds/${buildId}`);
6870
6878
  }
6879
+ async getVersion(versionId) {
6880
+ return this.request(`/control/versions/${versionId}`);
6881
+ }
6871
6882
  async triggerBuild(sandboxId, manifestId) {
6872
6883
  return this.request(`/control/sandboxes/${sandboxId}/builds`, {
6873
6884
  method: "POST",
6874
6885
  body: JSON.stringify({ manifestId })
6875
6886
  });
6876
6887
  }
6888
+ async createVersion(sandboxId, manifestId) {
6889
+ return this.triggerBuild(sandboxId, manifestId);
6890
+ }
6891
+ async listTags(sandboxId) {
6892
+ const result = await this.request(
6893
+ `/control/sandboxes/${sandboxId}/tags`
6894
+ );
6895
+ return result.items.map((tag2) => ({
6896
+ ...tag2,
6897
+ targetVersionId: tag2.targetVersionId ?? tag2.targetBuildId ?? null
6898
+ }));
6899
+ }
6900
+ async moveTag(tagId, targetBuildId) {
6901
+ const moved = await this.request(`/control/tags/${tagId}/move`, {
6902
+ method: "POST",
6903
+ body: JSON.stringify({ targetBuildId })
6904
+ });
6905
+ return {
6906
+ ...moved,
6907
+ targetVersionId: moved.targetVersionId ?? moved.targetBuildId ?? null
6908
+ };
6909
+ }
6910
+ async moveVersionTag(tagId, targetVersionId) {
6911
+ return this.moveTag(tagId, targetVersionId);
6912
+ }
6913
+ async promoteToProd(buildId) {
6914
+ return this.request(`/control/builds/${buildId}/promote`, {
6915
+ method: "POST"
6916
+ });
6917
+ }
6918
+ async promoteVersionToProd(versionId) {
6919
+ return this.request(`/control/versions/${versionId}/promote`, {
6920
+ method: "POST"
6921
+ });
6922
+ }
6923
+ async getBuildDiff(buildId, againstBuildId) {
6924
+ const query = againstBuildId ? `?against=${encodeURIComponent(againstBuildId)}` : "";
6925
+ return this.request(`/control/builds/${buildId}/diff${query}`);
6926
+ }
6927
+ async getVersionDiff(versionId, againstVersionId) {
6928
+ const query = againstVersionId ? `?against=${encodeURIComponent(againstVersionId)}` : "";
6929
+ const result = await this.request(`/control/versions/${versionId}/diff${query}`);
6930
+ return {
6931
+ versionId: result.buildId,
6932
+ againstVersionId: result.againstBuildId,
6933
+ diff: result.diff
6934
+ };
6935
+ }
6877
6936
  async waitForBuild(buildId, onStatus, timeoutMs = 12e4) {
6878
6937
  const start = Date.now();
6879
6938
  while (Date.now() - start < timeoutMs) {
@@ -6890,6 +6949,9 @@ var ApiClient = class {
6890
6949
  }
6891
6950
  throw new Error(`Build timed out after ${Math.round(timeoutMs / 1e3)}s`);
6892
6951
  }
6952
+ async waitForVersionBuild(versionId, onStatus, timeoutMs = 12e4) {
6953
+ return this.waitForBuild(versionId, onStatus, timeoutMs);
6954
+ }
6893
6955
  // ── Permission Profiles ──
6894
6956
  async createPermissionProfile(sandboxId, data) {
6895
6957
  return this.request(
@@ -7541,10 +7603,10 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
7541
7603
  return 3;
7542
7604
  }
7543
7605
  if ("TERM_PROGRAM" in env) {
7544
- const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
7606
+ const version2 = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
7545
7607
  switch (env.TERM_PROGRAM) {
7546
7608
  case "iTerm.app": {
7547
- return version >= 3 ? 3 : 2;
7609
+ return version2 >= 3 ? 3 : 2;
7548
7610
  }
7549
7611
  case "Apple_Terminal": {
7550
7612
  return 2;
@@ -8594,7 +8656,7 @@ var brand = {
8594
8656
  function printHeader() {
8595
8657
  console.log();
8596
8658
  console.log(brand.primary.bold(" \u25A0 Granular"));
8597
- console.log(brand.muted(" Build AI sandboxes from code"));
8659
+ console.log(brand.muted(" Version and run ontologies from code"));
8598
8660
  console.log();
8599
8661
  }
8600
8662
  function success(message) {
@@ -8776,10 +8838,11 @@ The rest of this guide assumes these terms.
8776
8838
  | Term | What it means |
8777
8839
  |------|----------------|
8778
8840
  | **Sandbox** | A **workspace** on Granular (\`sbx_\u2026\`). It stores the **compiled domain** and **your records** for one project/environment. Not a browser sandbox \u2014 think \u201Ctenant + schema + data\u201D for this app. |
8779
- | **Domain** | The **ontology**: **classes**, **relationships**, and **effects**. Declared in \`granular.json\`, installed on the sandbox by **build**. Same idea as *schema* / *model*. |
8780
- | **Manifest** | \`granular.json\` (definition under \`manifest\`). **Source** in the repo until you **build**. |
8781
- | **Build** | \`granular build\` \u2014 uploads and compiles the manifest. Primary **validation**; fixes errors before relying on types. |
8782
- | **Environment** | Return value of \`granular.connect({ sandbox, \u2026 })\`. Your **server-side** handle for \`recordObject\`, \`submitJob\`, \`graphql\`, etc., bound to one sandbox. |
8841
+ | **Domain** | The **ontology**: **classes**, **relationships**, and **effects**. Declared in \`granular.json\`, installed on the sandbox by creating a **version**. Same idea as *schema* / *model*. |
8842
+ | **Manifest** | \`granular.json\` (definition under \`manifest\`). **Source** in the repo until you create a new ontology **version** from it. |
8843
+ | **Version** | Immutable ontology state derived from one manifest revision. Tags like \`dev\` and \`prod\` point to versions. |
8844
+ | **Build run** | The CI-style compilation process. \`granular build\` creates or reuses the ontology version, then runs a build for it. |
8845
+ | **Environment** | Return value of \`granular.connect({ ontology, environment, \u2026 })\`. It is your **server-side** handle for \`recordObject\`, \`submitJob\`, \`graphql\`, etc., bound to one ontology environment slot. |
8783
8846
  | **Session** | SDK base type; \`Environment\` **extends** \`Session\`. Job/prompt APIs live on \`Session\`; you still use the **\`Environment\`** instance from \`connect()\` in normal apps. |
8784
8847
  | **Job** | Code string passed to \`environment.submitJob(code)\`. Runs in Granular\u2019s **sandbox runtime** with access to \`./sandbox-tools\` (generated classes + effect entrypoints). |
8785
8848
  | **Effect** | Declared with \`withEffect\`; **handler** registered with \`registerEffects\` in **your** process. Jobs invoke effects; handlers do IO outside Granular. |
@@ -8800,9 +8863,9 @@ function manifestGuideEndToEndSection() {
8800
8863
  | Step | Action | Outcome |
8801
8864
  |------|--------|---------|
8802
8865
  | 1 | Edit \`granular.json\` (\`manifest\` \u2192 \`volumes\` \u2192 \`operations\`) | Domain **source** in the repo |
8803
- | 2 | \`granular build\` | Manifest **uploaded**; domain **compiled** for the configured sandbox; errors surface here |
8866
+ | 2 | \`granular build\` | Manifest **uploaded**; ontology version created or reused; build run compiles it; errors surface here |
8804
8867
  | 3 | (Optional) Run your **effects host** \u2014 e.g. \`npx tsx granular-effects.ts\` \u2014 calling \`registerEffects(sandboxId, \u2026)\` | Effect **handlers** attached in **your** process |
8805
- | 4 | \`new Granular({ apiKey })\` then \`connect({ sandbox, userId, permissions })\` | **\`Environment\`** for that sandbox |
8868
+ | 4 | \`new Granular({ apiKey })\` then \`connect({ ontology, environment, userId, permissions })\` | **\`Environment\`** for that ontology environment slot |
8806
8869
  | 5 | \`recordObject\` / \`recordObjects\` | **Records** stored with correct fields and relationship keys |
8807
8870
  | 6 | \`submitJob(\`\u2026\`)\` with imports from \`./sandbox-tools\` | **Jobs** run; may call **effects** \u2192 your handlers execute and return results |
8808
8871
 
@@ -8814,8 +8877,10 @@ function sandboxDocMainConceptsSection() {
8814
8877
  | Term | One line |
8815
8878
  |------|----------|
8816
8879
  | **Sandbox** | Workspace \`sbx_\u2026\` \u2014 **domain** + **data** for this project. |
8817
- | **Domain / ontology** | Classes + relationships + effects \u2014 **built** from \`granular.json\`. |
8880
+ | **Domain / ontology** | Classes + relationships + effects \u2014 materialized from versioned manifests. |
8818
8881
  | **Manifest** | \`granular.json\` \u2014 **source**; edit here, then \`granular build\`. |
8882
+ | **Version** | Immutable ontology state created from one manifest revision. |
8883
+ | **Build run** | Compilation run that validates and materializes a version. |
8819
8884
  | **Environment** | \`connect()\` result \u2014 **record**, **submitJob**, **graphql** for this sandbox. |
8820
8885
  | **Session** | Base type; \`Environment\` extends it (job APIs). |
8821
8886
  | **Job** | \`submitJob\` code using \`./sandbox-tools\`. |
@@ -8833,10 +8898,29 @@ function sandboxDocScopeSection(projectName) {
8833
8898
 
8834
8899
  If **syntax** or **Granular behavior** is unclear, use the manifest guide; use **this** file for **ontology facts** for **${projectName}**.`;
8835
8900
  }
8901
+ function getFieldMetamodelDocRows() {
8902
+ return metamodelPresetDefault.collectMetamodelDocRows("fieldRows");
8903
+ }
8904
+ function getModelMetamodelDocRows() {
8905
+ return metamodelPresetDefault.collectMetamodelDocRows("modelRows");
8906
+ }
8907
+ function getEffectMetamodelDocRows() {
8908
+ return metamodelPresetDefault.collectMetamodelDocRows("effectRows");
8909
+ }
8910
+ function renderMetamodelDocTable(rows) {
8911
+ return [
8912
+ "| Key | Meaning |",
8913
+ "|-----|---------|",
8914
+ ...rows.map((row) => `| \`${row.key}\` | ${row.description} |`)
8915
+ ].join("\n");
8916
+ }
8836
8917
 
8837
8918
  // src/cli/agent-docs/manifest-guide.ts
8838
8919
  function generateManifestAgentGuide(options) {
8839
8920
  const { projectName } = options;
8921
+ const fieldMetamodelTable = renderMetamodelDocTable(getFieldMetamodelDocRows());
8922
+ const modelMetamodelTable = renderMetamodelDocTable(getModelMetamodelDocRows());
8923
+ const effectMetamodelTable = renderMetamodelDocTable(getEffectMetamodelDocRows());
8840
8924
  return `# Granular manifest guide (for coding agents)
8841
8925
 
8842
8926
  This guide is for **coding agents** who may have **no prior context on Granular**. It explains the product, core terms, how to **author** \`granular.json\`, **build** the domain, and use the SDK (\`connect\` \u2192 **environment**, \`recordObject\`, \`submitJob\`, **effects**). For **this repository\u2019s** exact class names, relationship keys, and effect payloads only, see [GRANULAR_SANDBOX.md](../GRANULAR_SANDBOX.md).
@@ -8942,6 +9026,66 @@ Each key under \`has\` is a **field name** on the class. Each value is a **field
8942
9026
  - Every scalar field should have \`"type": "..."\`.
8943
9027
  - Do **not** store links to other entities as opaque strings when you can use **relationships** (next section).
8944
9028
 
9029
+ ### Field metamodels
9030
+
9031
+ Scalar fields can carry lightweight metamodels directly in their field spec:
9032
+
9033
+ \`\`\`json
9034
+ {
9035
+ "create": "ticket",
9036
+ "extends": "@std/class",
9037
+ "note": "Ticket workflow object",
9038
+ "stateMachines": [
9039
+ {
9040
+ "name": "lifecycle",
9041
+ "entryState": "draft",
9042
+ "states": ["draft", "active", { "name": "done", "isFinal": true }],
9043
+ "finalStates": ["canceled"],
9044
+ "transitions": [
9045
+ { "name": "activate", "from": "draft", "to": "active" },
9046
+ { "name": "complete", "from": "active", "to": "done" },
9047
+ { "name": "cancel", "from": "draft", "to": "canceled" }
9048
+ ]
9049
+ }
9050
+ ],
9051
+ "has": {
9052
+ "title": {
9053
+ "type": "string",
9054
+ "required": true,
9055
+ "note": "Must use the canonical prefix",
9056
+ "filterBy": true,
9057
+ "validate": [
9058
+ { "operator": "regex", "stringValue": "^TKT-[A-Z0-9-]+$" }
9059
+ ]
9060
+ },
9061
+ "status": {
9062
+ "type": "string",
9063
+ "enum": {
9064
+ "values": ["draft", "active", "done", "canceled"],
9065
+ "message": "Invalid ticket status"
9066
+ },
9067
+ "filterBy": ["equal_to", "contains"]
9068
+ },
9069
+ "estimate": {
9070
+ "type": "number",
9071
+ "filterBy": true,
9072
+ "validate": [
9073
+ { "operator": "gt", "numberValue": 0 },
9074
+ { "operator": "lt", "numberValue": 100 }
9075
+ ]
9076
+ }
9077
+ }
9078
+ }
9079
+ \`\`\`
9080
+
9081
+ Supported field metamodel keys:
9082
+
9083
+ ${fieldMetamodelTable}
9084
+
9085
+ Supported model-level metamodel keys:
9086
+
9087
+ ${modelMetamodelTable}
9088
+
8945
9089
  ---
8946
9090
 
8947
9091
  ## Operations: \`defineRelationship\` (model links precisely)
@@ -9023,10 +9167,54 @@ Example:
9023
9167
  | \`isStatic\` | \`true\` \u2192 static method; \`false\` or omit \u2192 **instance** method (handler receives the object **id** first). |
9024
9168
  | \`inputSchema\` / \`outputSchema\` | JSON Schema; used for codegen and validation. |
9025
9169
 
9026
- **Lifecycle:** declare in manifest \u2192 \`granular build\` \u2192 implement handlers \u2192 \`granular.registerEffects(sandboxId, [...])\` \u2192 jobs call generated methods \u2192 your handler runs and returns the result to the job.
9170
+ **Lifecycle:** declare in manifest \u2192 \`granular build\` creates or reuses a version and runs the build \u2192 implement handlers \u2192 \`granular.registerEffects(sandboxId, [...])\` \u2192 jobs call generated methods \u2192 your handler runs and returns the result to the job.
9027
9171
 
9028
9172
  **Permissions:** \`connect({ permissions: [...] })\` assigns profiles (e.g. \`default\`) that can allow or deny which effects a user may call.
9029
9173
 
9174
+ ### Effect metamodels
9175
+
9176
+ Effects can declare runtime behaviors under \`withEffect.metamodels\`:
9177
+
9178
+ \`\`\`json
9179
+ {
9180
+ "withEffect": {
9181
+ "name": "publish_ticket",
9182
+ "attachedClass": "ticket",
9183
+ "inputSchema": {
9184
+ "type": "object",
9185
+ "properties": { "audience": { "type": "string" } }
9186
+ },
9187
+ "outputSchema": {
9188
+ "type": "object",
9189
+ "properties": { "ok": { "type": "boolean" } }
9190
+ },
9191
+ "metamodels": {
9192
+ "postCondition": {
9193
+ "condition": "result.ok == true",
9194
+ "description": "Publishing must report success"
9195
+ },
9196
+ "dryRun": {
9197
+ "enabled": true,
9198
+ "description": "Preview publishing before sending"
9199
+ },
9200
+ "reverse": {
9201
+ "handler": "unpublish_ticket",
9202
+ "description": "Undo a publication"
9203
+ },
9204
+ "approvalRequired": {
9205
+ "required": true,
9206
+ "reason": "Publishing is user-visible",
9207
+ "mode": "confirm"
9208
+ }
9209
+ }
9210
+ }
9211
+ }
9212
+ \`\`\`
9213
+
9214
+ Supported effect metamodel keys:
9215
+
9216
+ ${effectMetamodelTable}
9217
+
9030
9218
  ---
9031
9219
 
9032
9220
  ## IDs and labels
@@ -9048,7 +9236,7 @@ Example:
9048
9236
  |-----|---------|
9049
9237
  | \`new Granular({ apiKey, apiUrl?, endpointMode?, token?, tokenProvider?, \u2026 })\` | Auth. Env: \`GRANULAR_API_KEY\`, \`GRANULAR_API_URL\`, \`GRANULAR_ENDPOINT_MODE\`. |
9050
9238
  | \`recordUser({ userId, name?, email?, permissions? })\` | Upsert user for later \`connect\`. |
9051
- | \`connect({ sandbox, userId?, granularId?, user?, permissions?, clientId?, initialHeap? })\` | Opens a session \u2192 **\`Environment\`**. |
9239
+ | \`connect({ ontology, environment, tagName?, userId?, granularId?, user?, permissions?, clientId?, initialHeap? })\` | Opens a session \u2192 **\`Environment\`**. \`tagName\` is an advanced override. |
9052
9240
  | \`registerEffects(sandboxId, effects)\` / \`registerEffect\` | Register handlers for manifest \`withEffect\` declarations. |
9053
9241
  | \`unregisterEffect\` / \`unregisterAllEffects\` / \`disconnectEffects\` | Stop effect handlers for a sandbox. |
9054
9242
  | \`granular.sandboxes\` | \`.list()\`, \`.get\`, \`.create\`, \`.update\`, \`.delete\` |
@@ -9090,14 +9278,17 @@ Use \`environment.graphql(query, variables?)\` when you need **query/mutation ac
9090
9278
 
9091
9279
  | Command | Purpose |
9092
9280
  |---------|---------|
9093
- | \`granular build\` | Upload \`granular.json\` and compile (primary **validation**). |
9094
- | \`granular deploy\` | Build + deploy current production. |
9281
+ | \`granular build\` | Upload \`granular.json\`, create or reuse the matching ontology version, and run the build (**validation**). |
9282
+ | \`granular deploy\` | Push the current revision, build it, and update \`dev\`. |
9283
+ | \`granular deploy --prod\` | If the current version is already on \`dev\`, also point \`prod\` to it. Otherwise build it, then point both \`dev\` and \`prod\` to it. |
9284
+ | \`granular version diff\` | Show the semantic diff between ontology versions. |
9285
+ | \`granular tag move\` | Move \`dev\`, \`prod\`, or another tag to a chosen version. |
9095
9286
  | \`granular dev\` | Watch manifest, rebuild on change. |
9096
9287
  | \`granular document\` | Regenerate \`GRANULAR_SANDBOX.md\`. |
9097
9288
  | \`granular init\` | Scaffold project; optional \`--agent-docs\`. |
9098
9289
  | \`granular add class|field|relation\` | Mutate local manifest. |
9099
9290
  | \`granular simulate\` | Open simulator in browser. |
9100
- | \`granular pull\` | Pull manifest from API. |
9291
+ | \`granular pull <version>\` | Pull the manifest for a specific ontology version. |
9101
9292
  | \`granular login\`, \`granular whoami\`, \`granular status\` | Auth / project info. |
9102
9293
 
9103
9294
  ---
@@ -9106,7 +9297,7 @@ Use \`environment.graphql(query, variables?)\` when you need **query/mutation ac
9106
9297
 
9107
9298
  1. \`granular build\` (or \`granular dev\`).
9108
9299
  2. Align **effect handlers** with every \`withEffect\` name + schemas.
9109
- 3. \`granular document\` to refresh \`GRANULAR_SANDBOX.md\` without a full build if needed (build still records manifest/build ids).
9300
+ 3. \`granular document\` to refresh \`GRANULAR_SANDBOX.md\` without a full build if needed (the last successful version/build metadata still comes from \`granular build\` or \`granular deploy\`).
9110
9301
 
9111
9302
  ---
9112
9303
 
@@ -9191,9 +9382,9 @@ function generateSandboxAgentDoc(manifest, meta) {
9191
9382
  lines.push(`| Sandbox id | \`${meta.sandboxId}\` |`);
9192
9383
  if (meta.apiUrl) lines.push(`| API / WS base | \`${meta.apiUrl}\` |`);
9193
9384
  if (meta.manifestId) lines.push(`| Last uploaded manifest id | \`${meta.manifestId}\` |`);
9194
- if (meta.buildId) lines.push(`| Last build id | \`${meta.buildId}\` |`);
9385
+ if (meta.buildId) lines.push(`| Last version id | \`${meta.buildId}\` |`);
9195
9386
  if (meta.buildPending) {
9196
- lines.push("| Build status | *No successful build recorded in this doc run \u2014 run `granular build`.* |");
9387
+ lines.push("| Version status | *No successful version/build recorded in this doc run \u2014 run `granular build`.* |");
9197
9388
  }
9198
9389
  lines.push("");
9199
9390
  lines.push("Set `GRANULAR_API_KEY` (e.g. in `.env.local`). Optional: `GRANULAR_API_URL` overrides the WebSocket URL.");
@@ -9210,7 +9401,9 @@ function generateSandboxAgentDoc(manifest, meta) {
9210
9401
  lines.push("");
9211
9402
  lines.push("| Command | Purpose |");
9212
9403
  lines.push("|---------|---------|");
9213
- lines.push("| `granular build` | Upload manifest and build |");
9404
+ lines.push("| `granular build` | Create or rebuild the current ontology version |");
9405
+ lines.push("| `granular version diff <version> [base]` | Show the semantic diff between versions |");
9406
+ lines.push("| `granular tag move <tag> <version>` | Move a release tag such as `prod` |");
9214
9407
  lines.push("| `granular document` | Refresh this file from `granular.json` |");
9215
9408
  lines.push("| `npx tsx " + seed + "` | Seed sample records (if present) |");
9216
9409
  lines.push("| `npx tsx " + eff + "` | Run effect handler host (if present) |");
@@ -9226,7 +9419,8 @@ function generateSandboxAgentDoc(manifest, meta) {
9226
9419
  lines.push(`});`);
9227
9420
  lines.push("");
9228
9421
  lines.push(`const env = await granular.connect({`);
9229
- lines.push(` sandbox: '${meta.sandboxId}',`);
9422
+ lines.push(` ontology: '${meta.sandboxId}',`);
9423
+ lines.push(` environment: 'dev',`);
9230
9424
  lines.push(` userId: 'your_app_user_id',`);
9231
9425
  lines.push(` permissions: ['default'],`);
9232
9426
  lines.push(`});`);
@@ -9835,7 +10029,7 @@ async function loginCommand(options = {}) {
9835
10029
  }
9836
10030
 
9837
10031
  // src/cli/commands/pull.ts
9838
- async function pullCommand() {
10032
+ async function pullCommand(versionId) {
9839
10033
  printHeader();
9840
10034
  const config = resolveConfig({ requireApiKey: true });
9841
10035
  if (!config.sandboxId) {
@@ -9843,17 +10037,13 @@ async function pullCommand() {
9843
10037
  process.exit(1);
9844
10038
  }
9845
10039
  const api = new ApiClient(config.apiKey, config.apiUrl);
9846
- const spin = spinner("Fetching manifests...");
10040
+ const spin = spinner(`Fetching version ${versionId}...`);
9847
10041
  try {
9848
- const manifests = await api.listManifests(config.sandboxId);
9849
- if (manifests.length === 0) {
9850
- spin.fail(" No manifests found for this sandbox.");
9851
- hint("granular build", "Upload and build your local manifest");
9852
- return;
9853
- }
9854
- const latest = manifests[0];
10042
+ const version2 = await api.getVersion(versionId);
10043
+ const targetManifestId = version2.manifestId;
10044
+ const targetVersionLabel = version2.versionNumber != null ? `v${version2.versionNumber}` : versionId;
9855
10045
  spin.text = " Downloading manifest content...";
9856
- const full = await api.getManifest(latest.manifestId);
10046
+ const full = await api.getManifest(targetManifestId);
9857
10047
  if (!full.content) {
9858
10048
  spin.fail(" Manifest has no content.");
9859
10049
  return;
@@ -9865,8 +10055,9 @@ async function pullCommand() {
9865
10055
  spin.succeed(" Manifest pulled successfully.");
9866
10056
  console.log();
9867
10057
  keyValue({
9868
- "Manifest ID": latest.manifestId,
9869
- "Version": latest.version || "N/A",
10058
+ "Manifest ID": full.manifestId,
10059
+ "Source Version": versionId,
10060
+ "Version": targetVersionLabel,
9870
10061
  "Written to": "granular.json"
9871
10062
  });
9872
10063
  console.log();
@@ -9895,17 +10086,23 @@ async function buildCommand() {
9895
10086
  uploading.fail(` Failed to upload manifest: ${err.message}`);
9896
10087
  process.exit(1);
9897
10088
  }
9898
- const building = spinner("Triggering build...");
10089
+ const building = spinner("Creating or reusing ontology version from manifest...");
9899
10090
  try {
9900
- const build = await api.triggerBuild(config.sandboxId, uploadedManifest.manifestId);
9901
- building.text = ` Building... ${brand.muted(build.buildId)}`;
10091
+ const version2 = await api.createVersion(config.sandboxId, uploadedManifest.manifestId);
10092
+ building.text = ` Running build for version... ${brand.muted(version2.buildId)}`;
9902
10093
  const startTime = Date.now();
9903
- const completed = await api.waitForBuild(build.buildId, (status) => {
10094
+ const completed = await api.waitForVersionBuild(version2.buildId, (status) => {
9904
10095
  const elapsed = Math.round((Date.now() - startTime) / 1e3);
9905
- building.text = ` Building... ${brand.muted(`${status} (${elapsed}s)`)}`;
10096
+ building.text = ` Running build for version... ${brand.muted(`${status} (${elapsed}s)`)}`;
9906
10097
  });
9907
10098
  const totalTime = Math.round((Date.now() - startTime) / 1e3);
9908
- building.succeed(` Build completed in ${totalTime}s`);
10099
+ building.succeed(` Version ready in ${totalTime}s`);
10100
+ const [tags, diff] = await Promise.all([
10101
+ api.listTags(config.sandboxId),
10102
+ api.getVersionDiff(completed.buildId).catch(() => null)
10103
+ ]);
10104
+ const devTag = tags.find((tag2) => tag2.name === "dev");
10105
+ const prodTag = tags.find((tag2) => tag2.name === "prod");
9909
10106
  writeSandboxAgentDocFile(getProjectRoot(), manifest, {
9910
10107
  sandboxId: config.sandboxId,
9911
10108
  apiUrl: config.apiUrl,
@@ -9914,12 +10111,27 @@ async function buildCommand() {
9914
10111
  });
9915
10112
  console.log();
9916
10113
  keyValue({
9917
- "Build ID": completed.buildId,
10114
+ "Version ID": completed.buildId,
10115
+ "Version Number": completed.versionNumber != null ? String(completed.versionNumber) : "N/A",
10116
+ "Build Run": completed.buildRunId || completed.latestBuildRunId || "N/A",
9918
10117
  "Manifest": uploadedManifest.manifestId,
9919
- "Status": "completed",
10118
+ "Result": completed.createdNewVersion ? "new version created" : "existing version reused, new build run recorded",
10119
+ "Dev Tag": devTag?.targetVersionId === completed.buildId || devTag?.targetBuildId === completed.buildId ? "updated to this version" : devTag?.targetVersionId || devTag?.targetBuildId || "not set",
10120
+ "Prod Tag": prodTag?.targetVersionId || prodTag?.targetBuildId || "not set",
9920
10121
  "Duration": `${totalTime}s`,
9921
10122
  "Agent doc": "GRANULAR_SANDBOX.md"
9922
10123
  });
10124
+ if (diff?.diff?.summary) {
10125
+ console.log();
10126
+ keyValue({
10127
+ "Diff Added": String(diff.diff.summary.added ?? 0),
10128
+ "Diff Changed": String(diff.diff.summary.changed ?? 0),
10129
+ "Diff Removed": String(diff.diff.summary.removed ?? 0),
10130
+ "Transition Safety": diff.diff.summary.onlyAdditiveChanges ? "additive" : "breaking changes detected"
10131
+ });
10132
+ }
10133
+ console.log();
10134
+ info("Common path: keep working in your dev environment while dev follows the newest successful version. Prod only changes when you promote it.");
9923
10135
  console.log();
9924
10136
  } catch (err) {
9925
10137
  building.fail(` Build failed: ${err.message}`);
@@ -9928,7 +10140,7 @@ async function buildCommand() {
9928
10140
  }
9929
10141
 
9930
10142
  // src/cli/commands/deploy.ts
9931
- async function deployCommand() {
10143
+ async function deployCommand(options = {}) {
9932
10144
  printHeader();
9933
10145
  const config = resolveConfig({ requireApiKey: true, requireProject: true });
9934
10146
  if (!config.sandboxId) {
@@ -9936,7 +10148,7 @@ async function deployCommand() {
9936
10148
  process.exit(1);
9937
10149
  }
9938
10150
  const api = new ApiClient(config.apiKey, config.apiUrl);
9939
- step("Deploy", `Sandbox ${config.sandboxId}`);
10151
+ step(options.prod ? "Deploy to dev and prod" : "Deploy to dev", `Sandbox ${config.sandboxId}`);
9940
10152
  console.log();
9941
10153
  const manifest = config.project.manifest;
9942
10154
  const uploading = spinner(`Uploading manifest "${manifest.name}"...`);
@@ -9948,16 +10160,45 @@ async function deployCommand() {
9948
10160
  uploading.fail(` Failed to upload manifest: ${err.message}`);
9949
10161
  process.exit(1);
9950
10162
  }
9951
- const building = spinner("Building...");
10163
+ const building = spinner("Resolving ontology version from manifest...");
9952
10164
  try {
9953
- const build = await api.triggerBuild(config.sandboxId, uploadedManifest.manifestId);
9954
- const startTime = Date.now();
9955
- const completed = await api.waitForBuild(build.buildId, (status) => {
9956
- const elapsed = Math.round((Date.now() - startTime) / 1e3);
9957
- building.text = ` Building... ${brand.muted(`${status} (${elapsed}s)`)}`;
9958
- });
9959
- const totalTime = Math.round((Date.now() - startTime) / 1e3);
9960
- building.succeed(` Build completed in ${totalTime}s`);
10165
+ const [versions, tags] = await Promise.all([
10166
+ api.listVersions(config.sandboxId),
10167
+ api.listTags(config.sandboxId).catch(() => [])
10168
+ ]);
10169
+ const devTag = tags.find((tag2) => tag2.name === "dev");
10170
+ const prodTag = tags.find((tag2) => tag2.name === "prod");
10171
+ const existingVersion = versions.find((version2) => version2.manifestDigest === uploadedManifest.digest) || null;
10172
+ const existingVersionId = existingVersion?.buildId || null;
10173
+ const devVersionId = devTag?.targetVersionId || devTag?.targetBuildId || null;
10174
+ const reuseDevPromotion = Boolean(
10175
+ options.prod && existingVersion && existingVersion.status === "completed" && existingVersionId && devVersionId === existingVersionId
10176
+ );
10177
+ let completed = existingVersion;
10178
+ let totalTime = 0;
10179
+ let deployedToDev = false;
10180
+ if (reuseDevPromotion) {
10181
+ building.succeed(` Current manifest already matches ${brand.secondary(existingVersionId)} on dev.`);
10182
+ } else {
10183
+ const version2 = await api.createVersion(config.sandboxId, uploadedManifest.manifestId);
10184
+ const startTime = Date.now();
10185
+ building.text = ` Running build for version... ${brand.muted(version2.buildId)}`;
10186
+ completed = await api.waitForVersionBuild(version2.buildId, (status) => {
10187
+ const elapsed = Math.round((Date.now() - startTime) / 1e3);
10188
+ building.text = ` Running build for version... ${brand.muted(`${status} (${elapsed}s)`)}`;
10189
+ });
10190
+ totalTime = Math.round((Date.now() - startTime) / 1e3);
10191
+ deployedToDev = true;
10192
+ building.succeed(` Dev now points to ${brand.secondary(completed.buildId)} (${totalTime}s)`);
10193
+ }
10194
+ if (!completed) {
10195
+ throw new Error("Could not resolve the current ontology version.");
10196
+ }
10197
+ if (options.prod) {
10198
+ const promoting = spinner("Promoting prod...");
10199
+ await api.promoteVersionToProd(completed.buildId);
10200
+ promoting.succeed(` Prod now points to ${brand.secondary(completed.buildId)}`);
10201
+ }
9961
10202
  writeSandboxAgentDocFile(getProjectRoot(), manifest, {
9962
10203
  sandboxId: config.sandboxId,
9963
10204
  apiUrl: config.apiUrl,
@@ -9968,12 +10209,20 @@ async function deployCommand() {
9968
10209
  success(`Deployed ${brand.bold(manifest.name)} successfully!`);
9969
10210
  console.log();
9970
10211
  keyValue({
9971
- "Build": completed.buildId,
10212
+ "Version": completed.buildId,
10213
+ "Version Number": completed.versionNumber != null ? String(completed.versionNumber) : "N/A",
10214
+ "Build Run": completed.buildRunId || completed.latestBuildRunId || "N/A",
9972
10215
  "Manifest": uploadedManifest.manifestId,
9973
- "Status": "live",
10216
+ "Dev Tag": completed.buildId,
10217
+ "Prod Tag": options.prod ? completed.buildId : prodTag?.targetVersionId || prodTag?.targetBuildId || "unchanged",
10218
+ "Release": options.prod ? deployedToDev ? "dev and prod now point to this version" : "prod promoted to the version already on dev" : "dev updated to this version",
9974
10219
  "Agent doc": "GRANULAR_SANDBOX.md"
9975
10220
  });
9976
10221
  console.log();
10222
+ if (options.prod) {
10223
+ info("Existing prod environments keep their current version until they are transitioned.");
10224
+ console.log();
10225
+ }
9977
10226
  } catch (err) {
9978
10227
  building.fail(` Deploy failed: ${err.message}`);
9979
10228
  process.exit(1);
@@ -10001,21 +10250,30 @@ async function statusCommand() {
10001
10250
  "Created": sandbox.createdAt
10002
10251
  });
10003
10252
  console.log();
10004
- const builds = await api.listBuilds(config.sandboxId);
10253
+ const builds = await api.listVersions(config.sandboxId);
10254
+ const tags = await api.listTags(config.sandboxId).catch(() => []);
10255
+ const devTag = tags.find((tag2) => tag2.name === "dev");
10256
+ const prodTag = tags.find((tag2) => tag2.name === "prod");
10257
+ keyValue({
10258
+ "Dev Version": devTag?.targetVersionId || devTag?.targetBuildId || "not set",
10259
+ "Prod Version": prodTag?.targetVersionId || prodTag?.targetBuildId || "not set"
10260
+ });
10261
+ console.log();
10005
10262
  if (builds.length > 0) {
10006
- step("Recent Builds");
10263
+ step("Recent Versions");
10007
10264
  console.log();
10008
10265
  const recentBuilds = builds.slice(0, 5);
10009
10266
  table(
10010
- ["Build ID", "Status", "Created"],
10267
+ ["Version", "Status", "Users/Envs", "Created"],
10011
10268
  recentBuilds.map((b) => [
10012
10269
  b.buildId,
10013
10270
  buildStatus(b.status),
10271
+ `${b.environmentCount ?? 0}/${b.sessionCount ?? 0}`,
10014
10272
  b.createdAt ? String(b.createdAt) : "N/A"
10015
10273
  ])
10016
10274
  );
10017
10275
  } else {
10018
- dim("No builds yet. Run `granular build` to create one.");
10276
+ dim("No versions yet. Run `granular build` to create one.");
10019
10277
  }
10020
10278
  console.log();
10021
10279
  const manifests = await api.listManifests(config.sandboxId);
@@ -10357,18 +10615,145 @@ async function simulateCommand(sandboxIdArg, options) {
10357
10615
  openUrl(url);
10358
10616
  }
10359
10617
 
10618
+ // src/cli/commands/version.ts
10619
+ function formatVersionLabel(versionId, versionNumber) {
10620
+ return versionNumber != null ? `v${versionNumber}` : versionId;
10621
+ }
10622
+ async function versionDiffCommand(versionId, againstVersionId) {
10623
+ printHeader();
10624
+ const config = resolveConfig({ requireApiKey: true });
10625
+ const api = new ApiClient(config.apiKey, config.apiUrl);
10626
+ const spin = spinner(
10627
+ againstVersionId ? `Computing diff ${versionId} \u2190 ${againstVersionId}...` : `Computing diff for ${versionId}...`
10628
+ );
10629
+ try {
10630
+ const [version2, diffResult] = await Promise.all([
10631
+ api.getVersion(versionId),
10632
+ api.getVersionDiff(versionId, againstVersionId)
10633
+ ]);
10634
+ const baseVersion = diffResult.againstVersionId ? await api.getVersion(diffResult.againstVersionId).catch(() => null) : null;
10635
+ const summary = diffResult.diff.summary;
10636
+ spin.succeed(" Version diff ready.");
10637
+ console.log();
10638
+ keyValue({
10639
+ "Version": formatVersionLabel(version2.buildId, version2.versionNumber),
10640
+ "Compared against": baseVersion ? formatVersionLabel(baseVersion.buildId, baseVersion.versionNumber) : diffResult.againstVersionId || "previous version",
10641
+ "Added operations": summary.added,
10642
+ "Changed operations": summary.changed,
10643
+ "Removed operations": summary.removed,
10644
+ "Transition safety": summary.onlyAdditiveChanges ? "additive only" : "breaking changes detected"
10645
+ });
10646
+ if (diffResult.diff.entries.length === 0) {
10647
+ console.log();
10648
+ info("No ontology changes between these versions.");
10649
+ console.log();
10650
+ return;
10651
+ }
10652
+ console.log();
10653
+ step("Semantic diff");
10654
+ console.log();
10655
+ table(
10656
+ ["Change", "Kind", "Operation", "Safety"],
10657
+ diffResult.diff.entries.map((entry) => [
10658
+ entry.changeType,
10659
+ entry.kind,
10660
+ entry.label,
10661
+ entry.additive ? "safe-add" : "breaking"
10662
+ ])
10663
+ );
10664
+ console.log();
10665
+ } catch (err) {
10666
+ spin.fail(` Failed to compute diff: ${err.message}`);
10667
+ process.exit(1);
10668
+ }
10669
+ }
10670
+
10671
+ // src/cli/commands/tag.ts
10672
+ function renderVersionTarget(versionId) {
10673
+ if (!versionId) return "not set";
10674
+ return versionId;
10675
+ }
10676
+ async function tagListCommand() {
10677
+ printHeader();
10678
+ const config = resolveConfig({ requireApiKey: true });
10679
+ if (!config.sandboxId) {
10680
+ error("No ontology configured. Run `granular init` first.");
10681
+ process.exit(1);
10682
+ }
10683
+ const api = new ApiClient(config.apiKey, config.apiUrl);
10684
+ const spin = spinner("Loading tags...");
10685
+ try {
10686
+ const tags = await api.listTags(config.sandboxId);
10687
+ spin.succeed(" Tags loaded.");
10688
+ console.log();
10689
+ if (tags.length === 0) {
10690
+ dim("No tags defined for this ontology yet.");
10691
+ console.log();
10692
+ return;
10693
+ }
10694
+ table(
10695
+ ["Tag", "Target version", "Kind", "Protected"],
10696
+ tags.map((tag2) => [
10697
+ tag2.name,
10698
+ renderVersionTarget(tag2.targetVersionId || tag2.targetBuildId),
10699
+ tag2.kind,
10700
+ tag2.protected ? "yes" : "no"
10701
+ ])
10702
+ );
10703
+ console.log();
10704
+ } catch (err) {
10705
+ spin.fail(` Failed to load tags: ${err.message}`);
10706
+ process.exit(1);
10707
+ }
10708
+ }
10709
+ async function tagMoveCommand(tagName, versionId) {
10710
+ printHeader();
10711
+ const config = resolveConfig({ requireApiKey: true });
10712
+ if (!config.sandboxId) {
10713
+ error("No ontology configured. Run `granular init` first.");
10714
+ process.exit(1);
10715
+ }
10716
+ const api = new ApiClient(config.apiKey, config.apiUrl);
10717
+ const spin = spinner(`Moving ${tagName} to version ${versionId}...`);
10718
+ try {
10719
+ const tags = await api.listTags(config.sandboxId);
10720
+ const tag2 = tags.find((candidate) => candidate.name === tagName);
10721
+ if (!tag2) {
10722
+ spin.fail(` Tag "${tagName}" was not found for this ontology.`);
10723
+ process.exit(1);
10724
+ }
10725
+ const previousVersionId = tag2.targetVersionId || tag2.targetBuildId || null;
10726
+ const moved = await api.moveVersionTag(tag2.tagId, versionId);
10727
+ spin.succeed(` ${tagName} now points to ${versionId}.`);
10728
+ console.log();
10729
+ keyValue({
10730
+ "Tag": moved.name,
10731
+ "Previous version": previousVersionId || "not set",
10732
+ "Current version": moved.targetVersionId || moved.targetBuildId || "not set",
10733
+ "Protected": moved.protected ? "yes" : "no"
10734
+ });
10735
+ console.log();
10736
+ if (moved.name === "prod") {
10737
+ info("Existing prod environments keep their current version until they are transitioned.");
10738
+ console.log();
10739
+ } else if (moved.name === "dev") {
10740
+ info("dev is usually automatic. You only need to move it manually for exceptional release workflows.");
10741
+ console.log();
10742
+ }
10743
+ } catch (err) {
10744
+ spin.fail(` Failed to move tag: ${err.message}`);
10745
+ process.exit(1);
10746
+ }
10747
+ }
10748
+
10360
10749
  // src/cli/index.ts
10361
10750
  var VERSION = "0.2.0";
10362
10751
  var program2 = new Command();
10363
- program2.name("granular").description("Build and deploy AI sandboxes from code").version(VERSION, "-v, --version");
10364
- program2.option("--local", "Use local endpoints (localhost)").option("--prod", "Use production endpoints").option("--env <target>", "Endpoint target: local|production");
10752
+ program2.name("granular").description("Version and run Granular ontologies from code").version(VERSION, "-v, --version");
10753
+ program2.option("--local", "Use local endpoints (localhost)").option("--env <target>", "Endpoint target: local|production");
10365
10754
  program2.hook("preAction", () => {
10366
10755
  const opts = program2.opts();
10367
10756
  const normalizedEnv = opts.env?.trim().toLowerCase();
10368
- if (opts.local && opts.prod) {
10369
- error("Cannot use --local and --prod at the same time.");
10370
- process.exit(1);
10371
- }
10372
10757
  if (normalizedEnv && !["local", "production", "prod"].includes(normalizedEnv)) {
10373
10758
  error('Invalid --env value. Use "local" or "production".');
10374
10759
  process.exit(1);
@@ -10376,7 +10761,7 @@ program2.hook("preAction", () => {
10376
10761
  let mode;
10377
10762
  if (opts.local || normalizedEnv === "local") {
10378
10763
  mode = "local";
10379
- } else if (opts.prod || normalizedEnv === "production" || normalizedEnv === "prod") {
10764
+ } else if (normalizedEnv === "production" || normalizedEnv === "prod") {
10380
10765
  mode = "production";
10381
10766
  }
10382
10767
  if (mode) {
@@ -10411,15 +10796,15 @@ program2.command("login").description("Authenticate with Granular (browser by de
10411
10796
  process.exit(1);
10412
10797
  }
10413
10798
  });
10414
- program2.command("pull").description("Pull the latest manifest from the API").action(async () => {
10799
+ program2.command("pull <version-id>").description("Pull the manifest for a specific ontology version").action(async (versionId) => {
10415
10800
  try {
10416
- await pullCommand();
10801
+ await pullCommand(versionId);
10417
10802
  } catch (err) {
10418
10803
  error(err.message);
10419
10804
  process.exit(1);
10420
10805
  }
10421
10806
  });
10422
- program2.command("build").description("Upload manifest and trigger a build").action(async () => {
10807
+ program2.command("build").description("Create or reuse the ontology version for your current manifest, then run its build").action(async () => {
10423
10808
  try {
10424
10809
  await buildCommand();
10425
10810
  } catch (err) {
@@ -10427,15 +10812,15 @@ program2.command("build").description("Upload manifest and trigger a build").act
10427
10812
  process.exit(1);
10428
10813
  }
10429
10814
  });
10430
- program2.command("deploy").description("Build and deploy as the current production version").action(async () => {
10815
+ program2.command("deploy").description("Push the current revision to dev; use --prod to move prod there too").option("--prod", "Also point prod to the current version").action(async (opts) => {
10431
10816
  try {
10432
- await deployCommand();
10817
+ await deployCommand({ prod: opts.prod });
10433
10818
  } catch (err) {
10434
10819
  error(err.message);
10435
10820
  process.exit(1);
10436
10821
  }
10437
10822
  });
10438
- program2.command("status").description("Show sandbox and build status").action(async () => {
10823
+ program2.command("status").description("Show ontology versions, dev/prod, and manifest status").action(async () => {
10439
10824
  try {
10440
10825
  await statusCommand();
10441
10826
  } catch (err) {
@@ -10443,6 +10828,32 @@ program2.command("status").description("Show sandbox and build status").action(a
10443
10828
  process.exit(1);
10444
10829
  }
10445
10830
  });
10831
+ var version = program2.command("version").description("Inspect ontology versions");
10832
+ version.command("diff <version-id> [against-version-id]").description("Show the semantic diff between one version and another").action(async (versionId, againstVersionId) => {
10833
+ try {
10834
+ await versionDiffCommand(versionId, againstVersionId);
10835
+ } catch (err) {
10836
+ error(err.message);
10837
+ process.exit(1);
10838
+ }
10839
+ });
10840
+ var tag = program2.command("tag").description("Inspect and move release tags such as dev and prod");
10841
+ tag.command("list").description("List tags for the current ontology").action(async () => {
10842
+ try {
10843
+ await tagListCommand();
10844
+ } catch (err) {
10845
+ error(err.message);
10846
+ process.exit(1);
10847
+ }
10848
+ });
10849
+ tag.command("move <tag-name> <version-id>").description("Move a tag to a specific ontology version").action(async (tagName, versionId) => {
10850
+ try {
10851
+ await tagMoveCommand(tagName, versionId);
10852
+ } catch (err) {
10853
+ error(err.message);
10854
+ process.exit(1);
10855
+ }
10856
+ });
10446
10857
  var add = program2.command("add").description("Add schema elements to the local manifest");
10447
10858
  add.command("class <name>").description("Add a new class to the schema").action(async (name) => {
10448
10859
  try {