@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/README.md +14 -12
- package/dist/cli/index.js +486 -75
- package/dist/index.d.mts +215 -12
- package/dist/index.d.ts +215 -12
- package/dist/index.js +375 -75
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +374 -76
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -1
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
|
|
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@${
|
|
3492
|
+
console.log(`[dotenv@${version2}][WARN] ${message}`);
|
|
3492
3493
|
}
|
|
3493
3494
|
function _debug(message) {
|
|
3494
|
-
console.log(`[dotenv@${
|
|
3495
|
+
console.log(`[dotenv@${version2}][DEBUG] ${message}`);
|
|
3495
3496
|
}
|
|
3496
3497
|
function _log(message) {
|
|
3497
|
-
console.log(`[dotenv@${
|
|
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
|
-
|
|
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
|
|
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
|
|
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("
|
|
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 **
|
|
8780
|
-
| **Manifest** | \`granular.json\` (definition under \`manifest\`). **Source** in the repo until you **
|
|
8781
|
-
| **
|
|
8782
|
-
| **
|
|
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**;
|
|
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({
|
|
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
|
|
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({
|
|
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
|
|
9094
|
-
| \`granular deploy\` |
|
|
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
|
|
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
|
|
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
|
|
9385
|
+
if (meta.buildId) lines.push(`| Last version id | \`${meta.buildId}\` |`);
|
|
9195
9386
|
if (meta.buildPending) {
|
|
9196
|
-
lines.push("|
|
|
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` |
|
|
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(`
|
|
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(
|
|
10040
|
+
const spin = spinner(`Fetching version ${versionId}...`);
|
|
9847
10041
|
try {
|
|
9848
|
-
const
|
|
9849
|
-
|
|
9850
|
-
|
|
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(
|
|
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":
|
|
9869
|
-
"Version":
|
|
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("
|
|
10089
|
+
const building = spinner("Creating or reusing ontology version from manifest...");
|
|
9899
10090
|
try {
|
|
9900
|
-
const
|
|
9901
|
-
building.text = `
|
|
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.
|
|
10094
|
+
const completed = await api.waitForVersionBuild(version2.buildId, (status) => {
|
|
9904
10095
|
const elapsed = Math.round((Date.now() - startTime) / 1e3);
|
|
9905
|
-
building.text = `
|
|
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(`
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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("
|
|
10163
|
+
const building = spinner("Resolving ontology version from manifest...");
|
|
9952
10164
|
try {
|
|
9953
|
-
const
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
const
|
|
9960
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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.
|
|
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
|
|
10263
|
+
step("Recent Versions");
|
|
10007
10264
|
console.log();
|
|
10008
10265
|
const recentBuilds = builds.slice(0, 5);
|
|
10009
10266
|
table(
|
|
10010
|
-
["
|
|
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
|
|
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("
|
|
10364
|
-
program2.option("--local", "Use local endpoints (localhost)").option("--
|
|
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 (
|
|
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
|
|
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("
|
|
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("
|
|
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
|
|
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 {
|