@lunnoa/client 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -1
- package/dist/cli.js +1242 -107
- package/dist/index.cjs +473 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1294 -195
- package/dist/index.d.ts +1294 -195
- package/dist/index.js +471 -57
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +253 -26
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1051 -143
- package/dist/react.d.ts +1051 -143
- package/dist/react.js +253 -26
- package/dist/react.js.map +1 -1
- package/package.json +4 -2
- package/schemas/agent.schema.json +128 -0
- package/schemas/workflow.schema.json +156 -0
- package/skills/lunnoa-client/SKILL.md +178 -9
- package/skills/lunnoa-solution-design/SKILL.md +12 -5
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
-
import { mkdir as
|
|
5
|
-
import { resolve as
|
|
4
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
5
|
+
import { resolve as resolve6 } from "path";
|
|
6
6
|
|
|
7
7
|
// src/core/auth.ts
|
|
8
8
|
var API_KEY_PREFIX = "lna_";
|
|
@@ -32,16 +32,21 @@ var LunnoaApiError = class extends Error {
|
|
|
32
32
|
method;
|
|
33
33
|
/** Path of the failed request (without the base URL). */
|
|
34
34
|
path;
|
|
35
|
+
/** Present on definition-sync 409s when the server sends the current revision. */
|
|
36
|
+
currentRevision;
|
|
35
37
|
constructor(args) {
|
|
36
38
|
const detail = extractMessage(args.body);
|
|
39
|
+
const currentRevision = extractCurrentRevision(args.body);
|
|
40
|
+
const shown = [detail, currentRevision ? `currentRevision: ${currentRevision}` : void 0].filter(Boolean).join(" ");
|
|
37
41
|
super(
|
|
38
|
-
`Lunnoa API request failed: ${args.method} ${args.path} \u2192 ${args.status}${
|
|
42
|
+
`Lunnoa API request failed: ${args.method} ${args.path} \u2192 ${args.status}${shown ? ` (${shown})` : ""}`
|
|
39
43
|
);
|
|
40
44
|
this.name = "LunnoaApiError";
|
|
41
45
|
this.status = args.status;
|
|
42
46
|
this.body = args.body;
|
|
43
47
|
this.method = args.method;
|
|
44
48
|
this.path = args.path;
|
|
49
|
+
this.currentRevision = currentRevision;
|
|
45
50
|
}
|
|
46
51
|
/** True for 401 responses (missing, revoked, or expired credential). */
|
|
47
52
|
get isUnauthorized() {
|
|
@@ -60,6 +65,15 @@ var LunnoaApiError = class extends Error {
|
|
|
60
65
|
return this.status === 429;
|
|
61
66
|
}
|
|
62
67
|
};
|
|
68
|
+
function publicApiMessage(error) {
|
|
69
|
+
const message = extractMessage(error.body) ?? error.message;
|
|
70
|
+
return error.currentRevision ? `${message} (currentRevision: ${error.currentRevision})` : message;
|
|
71
|
+
}
|
|
72
|
+
function extractCurrentRevision(body) {
|
|
73
|
+
if (!body || typeof body !== "object") return void 0;
|
|
74
|
+
const revision = body.currentRevision;
|
|
75
|
+
return typeof revision === "string" ? revision : void 0;
|
|
76
|
+
}
|
|
63
77
|
function extractMessage(body) {
|
|
64
78
|
if (body && typeof body === "object") {
|
|
65
79
|
const record = body;
|
|
@@ -536,6 +550,121 @@ var ActionsResource = class {
|
|
|
536
550
|
}
|
|
537
551
|
};
|
|
538
552
|
|
|
553
|
+
// src/define/files.ts
|
|
554
|
+
var STEP_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
555
|
+
function hasFileAgentSignals(record) {
|
|
556
|
+
if (record.instructions && typeof record.instructions === "object") return true;
|
|
557
|
+
if (record.settings && typeof record.settings === "object") return true;
|
|
558
|
+
if ("workflows" in record || "agents" in record || "knowledge" in record || "variables" in record || "objects" in record || "objectTypes" in record || "mcpServers" in record || "a2aAgents" in record) {
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
return Array.isArray(record.tools) && record.tools.some(
|
|
562
|
+
(tool) => tool && typeof tool === "object" && !("kind" in tool) && !("use" in tool)
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
function isAgentFileShape(config) {
|
|
566
|
+
if (!config || typeof config !== "object") return false;
|
|
567
|
+
return hasFileAgentSignals(config);
|
|
568
|
+
}
|
|
569
|
+
function acceptAgentFile(config) {
|
|
570
|
+
const slug = config.slug?.trim();
|
|
571
|
+
if (!slug) throw new Error("defineAgent requires slug");
|
|
572
|
+
const instructions = config.instructions;
|
|
573
|
+
const fileRef = instructions && typeof instructions === "object" && typeof instructions.file === "string" && instructions.file.trim().length > 0;
|
|
574
|
+
if (typeof instructions !== "string" && !fileRef) {
|
|
575
|
+
throw new Error("defineAgent requires instructions");
|
|
576
|
+
}
|
|
577
|
+
const copy = { ...config };
|
|
578
|
+
delete copy.$schema;
|
|
579
|
+
delete copy.kind;
|
|
580
|
+
return { ...copy, kind: "agent", slug, instructions };
|
|
581
|
+
}
|
|
582
|
+
function assertStepKeys(steps, seen) {
|
|
583
|
+
for (const step of steps ?? []) {
|
|
584
|
+
if (!step?.use?.id?.trim()) {
|
|
585
|
+
throw new Error("workflow step requires use.id");
|
|
586
|
+
}
|
|
587
|
+
if (step.key !== void 0) {
|
|
588
|
+
if (!STEP_KEY_PATTERN.test(step.key) || step.key === "trigger") {
|
|
589
|
+
throw new Error(
|
|
590
|
+
`Step key "${step.key}" must start with a letter, use letters, digits, - or _, and not be "trigger".`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
if (seen.has(step.key)) {
|
|
594
|
+
throw new Error(`Step key "${step.key}" is used twice.`);
|
|
595
|
+
}
|
|
596
|
+
seen.add(step.key);
|
|
597
|
+
}
|
|
598
|
+
for (const path of step.paths ?? []) {
|
|
599
|
+
assertStepKeys(path.steps ?? [], seen);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function acceptWorkflowStepsFile(config) {
|
|
604
|
+
const slug = config.slug?.trim();
|
|
605
|
+
if (!slug) throw new Error("defineWorkflow requires slug");
|
|
606
|
+
if (!config.steps?.length) {
|
|
607
|
+
throw new Error("defineWorkflow requires at least one step");
|
|
608
|
+
}
|
|
609
|
+
if (config.trigger && !config.trigger.id?.trim()) {
|
|
610
|
+
throw new Error("defineWorkflow trigger requires id");
|
|
611
|
+
}
|
|
612
|
+
assertStepKeys(config.steps, /* @__PURE__ */ new Set());
|
|
613
|
+
const copy = { ...config };
|
|
614
|
+
delete copy.$schema;
|
|
615
|
+
delete copy.kind;
|
|
616
|
+
return {
|
|
617
|
+
...copy,
|
|
618
|
+
kind: "workflow",
|
|
619
|
+
format: "steps",
|
|
620
|
+
slug
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function acceptWorkflowGraphFile(config) {
|
|
624
|
+
const slug = config.slug?.trim();
|
|
625
|
+
if (!slug) throw new Error("defineWorkflow requires slug");
|
|
626
|
+
if (!Array.isArray(config.nodes) || config.nodes.length === 0) {
|
|
627
|
+
throw new Error("defineWorkflow graph requires nodes");
|
|
628
|
+
}
|
|
629
|
+
const keys = /* @__PURE__ */ new Set();
|
|
630
|
+
let triggers = 0;
|
|
631
|
+
for (const node of config.nodes) {
|
|
632
|
+
if (!node?.key || typeof node.key !== "string") {
|
|
633
|
+
throw new Error("graph node requires key");
|
|
634
|
+
}
|
|
635
|
+
if (!STEP_KEY_PATTERN.test(node.key)) {
|
|
636
|
+
throw new Error(
|
|
637
|
+
`Node key "${node.key}" must start with a letter, use letters, digits, - or _, and not be empty.`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
if (keys.has(node.key)) {
|
|
641
|
+
throw new Error(`Node key "${node.key}" is used twice.`);
|
|
642
|
+
}
|
|
643
|
+
keys.add(node.key);
|
|
644
|
+
if (node.nodeType === "trigger") triggers += 1;
|
|
645
|
+
}
|
|
646
|
+
if (triggers !== 1) {
|
|
647
|
+
throw new Error("defineWorkflow graph requires exactly one trigger");
|
|
648
|
+
}
|
|
649
|
+
for (const edge of config.edges ?? []) {
|
|
650
|
+
if (!keys.has(edge?.source) || !keys.has(edge?.target)) {
|
|
651
|
+
throw new Error(
|
|
652
|
+
`Edge from "${edge?.source ?? ""}" to "${edge?.target ?? ""}" references an unknown key.`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const copy = { ...config };
|
|
657
|
+
delete copy.$schema;
|
|
658
|
+
delete copy.kind;
|
|
659
|
+
return {
|
|
660
|
+
...copy,
|
|
661
|
+
kind: "workflow",
|
|
662
|
+
format: "graph",
|
|
663
|
+
slug,
|
|
664
|
+
edges: config.edges ?? []
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
539
668
|
// src/define/index.ts
|
|
540
669
|
var DEFINE_KINDS = {
|
|
541
670
|
connection: "connection",
|
|
@@ -546,6 +675,33 @@ var DEFINE_KINDS = {
|
|
|
546
675
|
agent: "agent",
|
|
547
676
|
entityType: "entityType"
|
|
548
677
|
};
|
|
678
|
+
var PATH_CONDITIONS = [
|
|
679
|
+
"contains",
|
|
680
|
+
"does_not_contain",
|
|
681
|
+
"equals",
|
|
682
|
+
"does_not_equal",
|
|
683
|
+
"exists",
|
|
684
|
+
"does_not_exist",
|
|
685
|
+
"starts_with",
|
|
686
|
+
"ends_with",
|
|
687
|
+
"is_empty",
|
|
688
|
+
"is_not_empty",
|
|
689
|
+
"comes_before",
|
|
690
|
+
"comes_after",
|
|
691
|
+
"is_in_the_past",
|
|
692
|
+
"is_in_the_future",
|
|
693
|
+
"is_true",
|
|
694
|
+
"is_false",
|
|
695
|
+
"is_truthy",
|
|
696
|
+
"is_falsey",
|
|
697
|
+
"is_greater_than",
|
|
698
|
+
"is_less_than",
|
|
699
|
+
"is_greater_than_or_equal_to",
|
|
700
|
+
"is_less_than_or_equal_to",
|
|
701
|
+
"matches_regex",
|
|
702
|
+
"does_not_match_regex"
|
|
703
|
+
];
|
|
704
|
+
var PATH_CONDITION_SET = new Set(PATH_CONDITIONS);
|
|
549
705
|
function resolveActionAppId(action) {
|
|
550
706
|
if (action.appId?.trim()) {
|
|
551
707
|
return action.appId.trim();
|
|
@@ -646,10 +802,24 @@ function unwrapTool(tool) {
|
|
|
646
802
|
}
|
|
647
803
|
return tool;
|
|
648
804
|
}
|
|
805
|
+
function syncFields(options) {
|
|
806
|
+
return {
|
|
807
|
+
...options?.baseRevision ? { baseRevision: options.baseRevision } : {},
|
|
808
|
+
...options?.force ? { force: true } : {}
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function idsOf(links) {
|
|
812
|
+
if (!links) return void 0;
|
|
813
|
+
return links.map((link) => link.id);
|
|
814
|
+
}
|
|
649
815
|
function agentDefinitionToUpsert(def, options) {
|
|
816
|
+
if (isAgentFileShape(def)) {
|
|
817
|
+
return fileAgentToUpsert(def, options);
|
|
818
|
+
}
|
|
650
819
|
const tools = [];
|
|
651
820
|
const workflowIds = [...options?.workflowIds ?? []];
|
|
652
|
-
|
|
821
|
+
const authored = def;
|
|
822
|
+
for (const tool of authored.tools ?? []) {
|
|
653
823
|
const unwrapped = unwrapTool(tool);
|
|
654
824
|
if (unwrapped.kind === DEFINE_KINDS.workflow) {
|
|
655
825
|
continue;
|
|
@@ -665,17 +835,65 @@ function agentDefinitionToUpsert(def, options) {
|
|
|
665
835
|
});
|
|
666
836
|
}
|
|
667
837
|
}
|
|
838
|
+
return {
|
|
839
|
+
slug: authored.slug,
|
|
840
|
+
name: authored.name,
|
|
841
|
+
description: authored.description,
|
|
842
|
+
instructions: authored.instructions,
|
|
843
|
+
model: authored.model,
|
|
844
|
+
aiConnectionId: authored.aiConnection?.id,
|
|
845
|
+
connectionIds: authored.connections?.map((c) => c.id).filter((id) => Boolean(id)),
|
|
846
|
+
workflowIds: workflowIds.length ? workflowIds : void 0,
|
|
847
|
+
tools,
|
|
848
|
+
codeSource: options?.codeSource ?? "generated",
|
|
849
|
+
...syncFields(options)
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
function fileAgentToUpsert(def, options) {
|
|
853
|
+
if (typeof def.instructions !== "string") {
|
|
854
|
+
throw new Error(
|
|
855
|
+
"Agent instructions are still a file reference. The CLI resolves these before push."
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
const tools = [];
|
|
859
|
+
for (const tool of def.tools ?? []) {
|
|
860
|
+
const action = {
|
|
861
|
+
kind: DEFINE_KINDS.action,
|
|
862
|
+
id: tool.id,
|
|
863
|
+
appId: tool.appId,
|
|
864
|
+
connection: tool.connection?.id ? { kind: DEFINE_KINDS.connection, id: tool.connection.id } : void 0
|
|
865
|
+
};
|
|
866
|
+
tools.push({
|
|
867
|
+
key: tool.key,
|
|
868
|
+
appId: resolveActionAppId(action),
|
|
869
|
+
actionId: resolveActionId(action),
|
|
870
|
+
connectionId: tool.connection?.id,
|
|
871
|
+
name: tool.name,
|
|
872
|
+
description: tool.description,
|
|
873
|
+
input: tool.input
|
|
874
|
+
});
|
|
875
|
+
}
|
|
668
876
|
return {
|
|
669
877
|
slug: def.slug,
|
|
670
878
|
name: def.name,
|
|
671
|
-
description
|
|
879
|
+
// A file is the whole definition. Omitted description and model are empty.
|
|
880
|
+
description: def.description ?? null,
|
|
672
881
|
instructions: def.instructions,
|
|
673
|
-
model: def.model,
|
|
882
|
+
model: def.model ?? null,
|
|
674
883
|
aiConnectionId: def.aiConnection?.id,
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
884
|
+
...def.settings ? { settings: def.settings } : {},
|
|
885
|
+
...def.tools ? { tools } : {},
|
|
886
|
+
...def.workflows ? { workflows: def.workflows } : {},
|
|
887
|
+
...def.agents ? { agents: def.agents } : {},
|
|
888
|
+
...def.connections ? { connectionIds: idsOf(def.connections) } : {},
|
|
889
|
+
...def.knowledge ? { knowledgeIds: idsOf(def.knowledge) } : {},
|
|
890
|
+
...def.variables ? { variableIds: idsOf(def.variables) } : {},
|
|
891
|
+
...def.objects ? { objectIds: idsOf(def.objects) } : {},
|
|
892
|
+
...def.objectTypes ? { objectTypeIds: idsOf(def.objectTypes) } : {},
|
|
893
|
+
...def.mcpServers ? { mcpServers: def.mcpServers } : {},
|
|
894
|
+
...def.a2aAgents ? { a2aAgentIds: idsOf(def.a2aAgents) } : {},
|
|
895
|
+
codeSource: "synced",
|
|
896
|
+
...syncFields(options)
|
|
679
897
|
};
|
|
680
898
|
}
|
|
681
899
|
var AgentsResource = class {
|
|
@@ -695,15 +913,22 @@ var AgentsResource = class {
|
|
|
695
913
|
query: serializeListQuery(options)
|
|
696
914
|
});
|
|
697
915
|
}
|
|
916
|
+
/** Exported definition for `lunnoa/agents/<slug>.json`. */
|
|
917
|
+
getDefinition(slug) {
|
|
918
|
+
return this.http.get(
|
|
919
|
+
`/api/agents/slug/${encodeURIComponent(slug)}/definition`
|
|
920
|
+
);
|
|
921
|
+
}
|
|
698
922
|
/**
|
|
699
923
|
* Upsert an agent by slug inside the workspace on the API key.
|
|
700
924
|
* Prefer the CLI (`npx @lunnoa/client agents deploy`) for happy-path deploy.
|
|
701
925
|
*/
|
|
702
926
|
upsertBySlug(body, options) {
|
|
703
927
|
const payload = "kind" in body && body.kind === "agent" ? agentDefinitionToUpsert(body, options) : body;
|
|
704
|
-
return this.http.post(
|
|
705
|
-
|
|
706
|
-
|
|
928
|
+
return this.http.post(
|
|
929
|
+
"/api/agents/upsert",
|
|
930
|
+
{ body: payload }
|
|
931
|
+
);
|
|
707
932
|
}
|
|
708
933
|
};
|
|
709
934
|
|
|
@@ -1500,7 +1725,7 @@ function computeDurationMs(start, end) {
|
|
|
1500
1725
|
return b - a;
|
|
1501
1726
|
}
|
|
1502
1727
|
function defaultSleep(ms) {
|
|
1503
|
-
return new Promise((
|
|
1728
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
1504
1729
|
}
|
|
1505
1730
|
|
|
1506
1731
|
// src/resources/knowledge.ts
|
|
@@ -1805,6 +2030,11 @@ function stepFromDefinition(step) {
|
|
|
1805
2030
|
...step.name ? { name: step.name } : {}
|
|
1806
2031
|
};
|
|
1807
2032
|
}
|
|
2033
|
+
if (step.paths?.length) {
|
|
2034
|
+
throw new Error(
|
|
2035
|
+
"runs.start runs straight lines only. Deploy workflows with paths, then execute them."
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
1808
2038
|
const use = step.use;
|
|
1809
2039
|
const input = {
|
|
1810
2040
|
...use.input ?? {},
|
|
@@ -1822,6 +2052,11 @@ function stepFromDefinition(step) {
|
|
|
1822
2052
|
}
|
|
1823
2053
|
function normalizeStartRunInput(input) {
|
|
1824
2054
|
if (isWorkflowDefinition(input)) {
|
|
2055
|
+
if ("format" in input && input.format === "graph") {
|
|
2056
|
+
throw new Error(
|
|
2057
|
+
"runs.start runs straight lines only. Push the graph workflow, then execute it."
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
1825
2060
|
return {
|
|
1826
2061
|
steps: input.steps.map(stepFromDefinition),
|
|
1827
2062
|
name: input.name ?? input.slug
|
|
@@ -2013,6 +2248,18 @@ var WorkflowAppsResource = class {
|
|
|
2013
2248
|
}
|
|
2014
2249
|
};
|
|
2015
2250
|
|
|
2251
|
+
// src/resources/code-sync.ts
|
|
2252
|
+
var CodeSyncResource = class {
|
|
2253
|
+
constructor(http) {
|
|
2254
|
+
this.http = http;
|
|
2255
|
+
}
|
|
2256
|
+
http;
|
|
2257
|
+
/** Workspace id and whether synced resources are locked in the UI. */
|
|
2258
|
+
context() {
|
|
2259
|
+
return this.http.get("/api/code-sync/context");
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2262
|
+
|
|
2016
2263
|
// src/resources/workflows.ts
|
|
2017
2264
|
function triggerToUpsertInput(trigger) {
|
|
2018
2265
|
return {
|
|
@@ -2025,8 +2272,38 @@ function triggerToUpsertInput(trigger) {
|
|
|
2025
2272
|
...trigger.description ? { description: trigger.description } : {}
|
|
2026
2273
|
};
|
|
2027
2274
|
}
|
|
2275
|
+
function asAction(use) {
|
|
2276
|
+
if (use && typeof use === "object" && "kind" in use && use.kind === DEFINE_KINDS.action) {
|
|
2277
|
+
return use;
|
|
2278
|
+
}
|
|
2279
|
+
const fileUse = use;
|
|
2280
|
+
return {
|
|
2281
|
+
kind: DEFINE_KINDS.action,
|
|
2282
|
+
id: fileUse.id,
|
|
2283
|
+
appId: fileUse.appId,
|
|
2284
|
+
connection: fileUse.connection?.id ? { kind: DEFINE_KINDS.connection, id: fileUse.connection.id } : void 0
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
function asTrigger(trigger) {
|
|
2288
|
+
if ("kind" in trigger && trigger.kind === DEFINE_KINDS.trigger) {
|
|
2289
|
+
return trigger;
|
|
2290
|
+
}
|
|
2291
|
+
const fileTrigger = trigger;
|
|
2292
|
+
return {
|
|
2293
|
+
kind: DEFINE_KINDS.trigger,
|
|
2294
|
+
id: fileTrigger.id,
|
|
2295
|
+
appId: fileTrigger.appId,
|
|
2296
|
+
connection: fileTrigger.connection?.id ? { kind: DEFINE_KINDS.connection, id: fileTrigger.connection.id } : void 0,
|
|
2297
|
+
input: fileTrigger.input,
|
|
2298
|
+
inputs: fileTrigger.inputs,
|
|
2299
|
+
name: fileTrigger.name,
|
|
2300
|
+
description: fileTrigger.description
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2028
2303
|
function stepToUpsertInput(step) {
|
|
2029
|
-
const use = step.use;
|
|
2304
|
+
const use = asAction(step.use);
|
|
2305
|
+
const retry = "retrySettings" in step ? step.retrySettings : void 0;
|
|
2306
|
+
const continueOnFailure = "continueOnFailure" in step ? step.continueOnFailure : void 0;
|
|
2030
2307
|
return {
|
|
2031
2308
|
...step.key ? { key: step.key } : {},
|
|
2032
2309
|
appId: resolveActionAppId(use),
|
|
@@ -2037,18 +2314,60 @@ function stepToUpsertInput(step) {
|
|
|
2037
2314
|
...step.input ?? {}
|
|
2038
2315
|
},
|
|
2039
2316
|
name: step.name ?? use.name,
|
|
2040
|
-
description: use.description
|
|
2317
|
+
description: step.description ?? use.description,
|
|
2318
|
+
...retry ? { retrySettings: retry } : {},
|
|
2319
|
+
...continueOnFailure ? { continueOnFailure } : {},
|
|
2320
|
+
...step.paths?.length ? {
|
|
2321
|
+
paths: step.paths.map((path) => ({
|
|
2322
|
+
label: path.label,
|
|
2323
|
+
...path.when ? { when: path.when } : {},
|
|
2324
|
+
steps: path.steps.map(
|
|
2325
|
+
(nested) => stepToUpsertInput(nested)
|
|
2326
|
+
)
|
|
2327
|
+
}))
|
|
2328
|
+
} : {}
|
|
2041
2329
|
};
|
|
2042
2330
|
}
|
|
2043
|
-
function
|
|
2331
|
+
function syncFields2(options) {
|
|
2044
2332
|
return {
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2333
|
+
...options?.baseRevision ? { baseRevision: options.baseRevision } : {},
|
|
2334
|
+
...options?.force ? { force: true } : {}
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
function workflowDefinitionToUpsert(def, options) {
|
|
2338
|
+
if ("format" in def && def.format === "graph") {
|
|
2339
|
+
return {
|
|
2340
|
+
slug: def.slug,
|
|
2341
|
+
name: def.name,
|
|
2342
|
+
// A file is the whole definition: an omitted description is empty, not "leave it".
|
|
2343
|
+
description: def.description ?? null,
|
|
2344
|
+
codeSource: "synced",
|
|
2345
|
+
graph: { nodes: def.nodes, edges: def.edges ?? [] },
|
|
2346
|
+
...syncFields2(options)
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
if ("format" in def && def.format === "steps") {
|
|
2350
|
+
const file = def;
|
|
2351
|
+
return {
|
|
2352
|
+
slug: file.slug,
|
|
2353
|
+
name: file.name,
|
|
2354
|
+
description: file.description ?? null,
|
|
2355
|
+
...file.trigger ? { trigger: triggerToUpsertInput(asTrigger(file.trigger)) } : {},
|
|
2356
|
+
codeSource: "synced",
|
|
2357
|
+
steps: file.steps.map((step) => stepToUpsertInput(step)),
|
|
2358
|
+
...syncFields2(options)
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
const authored = def;
|
|
2362
|
+
return {
|
|
2363
|
+
slug: authored.slug,
|
|
2364
|
+
name: authored.name,
|
|
2365
|
+
description: authored.description,
|
|
2366
|
+
isActive: authored.isActive,
|
|
2367
|
+
...authored.trigger ? { trigger: triggerToUpsertInput(authored.trigger) } : {},
|
|
2368
|
+
codeSource: options?.codeSource ?? "generated",
|
|
2369
|
+
...syncFields2(options),
|
|
2370
|
+
steps: authored.steps.map((step) => stepToUpsertInput(step))
|
|
2052
2371
|
};
|
|
2053
2372
|
}
|
|
2054
2373
|
var WorkflowsResource = class {
|
|
@@ -2073,15 +2392,22 @@ var WorkflowsResource = class {
|
|
|
2073
2392
|
query: serializeListQuery(options)
|
|
2074
2393
|
});
|
|
2075
2394
|
}
|
|
2395
|
+
/** Exported definition for `lunnoa/workflows/<slug>.json`. */
|
|
2396
|
+
getDefinition(slug) {
|
|
2397
|
+
return this.http.get(
|
|
2398
|
+
`/api/workflows/slug/${encodeURIComponent(slug)}/definition`
|
|
2399
|
+
);
|
|
2400
|
+
}
|
|
2076
2401
|
/**
|
|
2077
2402
|
* Upsert a workflow by slug inside the workspace on the API key.
|
|
2078
2403
|
* Prefer the CLI (`npx @lunnoa/client workflows deploy`) for happy-path deploy.
|
|
2079
2404
|
*/
|
|
2080
|
-
upsertBySlug(body) {
|
|
2081
|
-
const payload = "kind" in body && body.kind === "workflow" ? workflowDefinitionToUpsert(body) : body;
|
|
2082
|
-
return this.http.post(
|
|
2083
|
-
|
|
2084
|
-
|
|
2405
|
+
upsertBySlug(body, options) {
|
|
2406
|
+
const payload = "kind" in body && body.kind === "workflow" ? workflowDefinitionToUpsert(body, options) : body;
|
|
2407
|
+
return this.http.post(
|
|
2408
|
+
"/api/workflows/upsert",
|
|
2409
|
+
{ body: payload }
|
|
2410
|
+
);
|
|
2085
2411
|
}
|
|
2086
2412
|
/**
|
|
2087
2413
|
* Starts a new execution of the workflow with the given trigger inputs.
|
|
@@ -2147,6 +2473,8 @@ var LunnoaClient = class {
|
|
|
2147
2473
|
aiConnections;
|
|
2148
2474
|
workflowApps;
|
|
2149
2475
|
discovery;
|
|
2476
|
+
/** Workspace id for the definitions lock, and the code-sync lock flag. */
|
|
2477
|
+
codeSync;
|
|
2150
2478
|
constructor(options) {
|
|
2151
2479
|
this.http = new HttpClient(options);
|
|
2152
2480
|
this.auth = new AuthResource(this.http, this.http.tokenStore);
|
|
@@ -2170,6 +2498,7 @@ var LunnoaClient = class {
|
|
|
2170
2498
|
this.connections = new ConnectionsResource(this.http);
|
|
2171
2499
|
this.aiConnections = new AiConnectionsResource(this.http);
|
|
2172
2500
|
this.discovery = new DiscoveryResource(this.http);
|
|
2501
|
+
this.codeSync = new CodeSyncResource(this.http);
|
|
2173
2502
|
}
|
|
2174
2503
|
};
|
|
2175
2504
|
|
|
@@ -2191,7 +2520,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
2191
2520
|
"--access-token",
|
|
2192
2521
|
"--out",
|
|
2193
2522
|
"--app",
|
|
2194
|
-
"--template"
|
|
2523
|
+
"--template",
|
|
2524
|
+
"--dir"
|
|
2195
2525
|
]);
|
|
2196
2526
|
function requireFlagValue(flag, value) {
|
|
2197
2527
|
if (!value || value.startsWith("--")) {
|
|
@@ -2205,6 +2535,8 @@ function parseArgs(argv) {
|
|
|
2205
2535
|
out: "./src/lunnoa",
|
|
2206
2536
|
json: false,
|
|
2207
2537
|
forAgent: false,
|
|
2538
|
+
all: false,
|
|
2539
|
+
force: false,
|
|
2208
2540
|
help: false
|
|
2209
2541
|
};
|
|
2210
2542
|
let i = 1;
|
|
@@ -2233,6 +2565,14 @@ function parseArgs(argv) {
|
|
|
2233
2565
|
args.forAgent = true;
|
|
2234
2566
|
continue;
|
|
2235
2567
|
}
|
|
2568
|
+
if (flag === "--all") {
|
|
2569
|
+
args.all = true;
|
|
2570
|
+
continue;
|
|
2571
|
+
}
|
|
2572
|
+
if (flag === "--force") {
|
|
2573
|
+
args.force = true;
|
|
2574
|
+
continue;
|
|
2575
|
+
}
|
|
2236
2576
|
if (flag && VALUE_FLAGS.has(flag)) {
|
|
2237
2577
|
const value = requireFlagValue(flag, argv[i + 1]);
|
|
2238
2578
|
switch (flag) {
|
|
@@ -2254,6 +2594,9 @@ function parseArgs(argv) {
|
|
|
2254
2594
|
case "--template":
|
|
2255
2595
|
args.template = value;
|
|
2256
2596
|
break;
|
|
2597
|
+
case "--dir":
|
|
2598
|
+
args.dir = value;
|
|
2599
|
+
break;
|
|
2257
2600
|
default:
|
|
2258
2601
|
break;
|
|
2259
2602
|
}
|
|
@@ -2292,6 +2635,12 @@ var USAGE = `Usage:
|
|
|
2292
2635
|
npx @lunnoa/client workflows deploy <path> --url <url> --api-key <key>
|
|
2293
2636
|
npx @lunnoa/client agents deploy <path> --url <url> --api-key <key>
|
|
2294
2637
|
npx @lunnoa/client entity-types deploy <path> --url <url> --api-key <key>
|
|
2638
|
+
.json is pushed (code synced). .ts / .js / .mjs is deployed from TypeScript (code generated).
|
|
2639
|
+
|
|
2640
|
+
Definitions folder (default ./lunnoa, or definitionsDir in lunnoa.config.*, or --dir):
|
|
2641
|
+
npx @lunnoa/client pull [--all | <slug> | agents/<slug> | workflows/<slug>] [--force]
|
|
2642
|
+
npx @lunnoa/client push [<path|slug>] [--force]
|
|
2643
|
+
npx @lunnoa/client status [--json]
|
|
2295
2644
|
|
|
2296
2645
|
Agent skills (copies this version's skills into .claude/skills and .cursor/skills):
|
|
2297
2646
|
npx @lunnoa/client skills install [projectDir]
|
|
@@ -2305,6 +2654,9 @@ Options:
|
|
|
2305
2654
|
--for-agent Only catalogue entries with availableForAgent
|
|
2306
2655
|
--template init starter: GitHub owner/repo[#ref] or local folder (default: lunnoa-automate/lunnoa-starter)
|
|
2307
2656
|
--json Machine-readable JSON (preferred for coding agents)
|
|
2657
|
+
--dir Definitions folder (default: ./lunnoa or definitionsDir in lunnoa.config)
|
|
2658
|
+
--all pull every agent and workflow in the workspace
|
|
2659
|
+
--force pull: overwrite a local edit; push: skip the revision check
|
|
2308
2660
|
--help Show this help
|
|
2309
2661
|
|
|
2310
2662
|
Environment: LUNNOA_URL, LUNNOA_API_KEY (also read from ./.env)
|
|
@@ -2321,6 +2673,682 @@ A slug already granted to other workspaces is not rewritten. Unknown instance at
|
|
|
2321
2673
|
allowUnknownAttributes is true; use extensions.
|
|
2322
2674
|
`;
|
|
2323
2675
|
|
|
2676
|
+
// src/cli/print.ts
|
|
2677
|
+
function printJson(value) {
|
|
2678
|
+
console.log(JSON.stringify(value, null, 2));
|
|
2679
|
+
}
|
|
2680
|
+
function formatTable(rows, columns) {
|
|
2681
|
+
if (rows.length === 0) {
|
|
2682
|
+
return "No results.";
|
|
2683
|
+
}
|
|
2684
|
+
const widths = columns.map((col) => {
|
|
2685
|
+
let width = col.header.length;
|
|
2686
|
+
for (const row of rows) {
|
|
2687
|
+
width = Math.max(width, stringifyCell(row[col.key]).length);
|
|
2688
|
+
}
|
|
2689
|
+
return width;
|
|
2690
|
+
});
|
|
2691
|
+
const header = columns.map((col, i) => pad(col.header, widths[i])).join(" ");
|
|
2692
|
+
const divider = widths.map((w) => "-".repeat(w)).join(" ");
|
|
2693
|
+
const body = rows.map(
|
|
2694
|
+
(row) => columns.map((col, i) => pad(stringifyCell(row[col.key]), widths[i])).join(" ")
|
|
2695
|
+
);
|
|
2696
|
+
return [header, divider, ...body].join("\n");
|
|
2697
|
+
}
|
|
2698
|
+
function printTable(rows, columns) {
|
|
2699
|
+
console.log(formatTable(rows, columns));
|
|
2700
|
+
}
|
|
2701
|
+
function stringifyCell(value) {
|
|
2702
|
+
if (value === null || value === void 0) {
|
|
2703
|
+
return "";
|
|
2704
|
+
}
|
|
2705
|
+
if (typeof value === "boolean") {
|
|
2706
|
+
return value ? "yes" : "no";
|
|
2707
|
+
}
|
|
2708
|
+
return String(value);
|
|
2709
|
+
}
|
|
2710
|
+
function pad(value, width) {
|
|
2711
|
+
return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
// src/cli/sync/engine.ts
|
|
2715
|
+
import { resolve as resolve2 } from "path";
|
|
2716
|
+
|
|
2717
|
+
// src/cli/sync/diff.ts
|
|
2718
|
+
var SECRET_NAME = /authorization|token|secret|password|passwd|api[-_]?key|bearer|cookie|private[-_]?key/i;
|
|
2719
|
+
function findSecretFields(definition) {
|
|
2720
|
+
if (!definition || typeof definition !== "object") return [];
|
|
2721
|
+
const record = definition;
|
|
2722
|
+
const hits = [];
|
|
2723
|
+
if (Array.isArray(record.tools)) {
|
|
2724
|
+
record.tools.forEach((tool, index) => {
|
|
2725
|
+
walk(tool?.input, `tools[${index}].input`, hits);
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
if (record.trigger && typeof record.trigger === "object") {
|
|
2729
|
+
walk(record.trigger.input, "trigger.input", hits);
|
|
2730
|
+
}
|
|
2731
|
+
if (Array.isArray(record.steps)) {
|
|
2732
|
+
record.steps.forEach((step, index) => walkStep(step, `steps[${index}]`, hits));
|
|
2733
|
+
}
|
|
2734
|
+
if (Array.isArray(record.nodes)) {
|
|
2735
|
+
record.nodes.forEach((node, index) => {
|
|
2736
|
+
walk(node?.value, `nodes[${index}].value`, hits);
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
return hits;
|
|
2740
|
+
}
|
|
2741
|
+
function walkStep(step, path, hits) {
|
|
2742
|
+
if (!step || typeof step !== "object") return;
|
|
2743
|
+
const record = step;
|
|
2744
|
+
walk(record.input, `${path}.input`, hits);
|
|
2745
|
+
record.paths?.forEach((pathDef, index) => {
|
|
2746
|
+
pathDef?.steps?.forEach((nested, stepIndex) => {
|
|
2747
|
+
walkStep(nested, `${path}.paths[${index}].steps[${stepIndex}]`, hits);
|
|
2748
|
+
});
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
function walk(value, path, hits) {
|
|
2752
|
+
if (Array.isArray(value)) {
|
|
2753
|
+
value.forEach((item, index) => walk(item, `${path}[${index}]`, hits));
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
if (!value || typeof value !== "object") return;
|
|
2757
|
+
for (const [key, child] of Object.entries(value)) {
|
|
2758
|
+
const childPath = `${path}.${key}`;
|
|
2759
|
+
if (typeof child === "string" && child.trim() && SECRET_NAME.test(key) && !child.includes("={{")) {
|
|
2760
|
+
hits.push(childPath);
|
|
2761
|
+
} else {
|
|
2762
|
+
walk(child, childPath, hits);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
function redactSecrets(text) {
|
|
2767
|
+
return text.replace(
|
|
2768
|
+
/("(?:\\.|[^"\\])*(?:authorization|token|secret|password|passwd|api[-_]?key|bearer|cookie|private[-_]?key)(?:\\.|[^"\\])*")(\s*:\s*)"((?:\\.|[^"\\])*)"/gi,
|
|
2769
|
+
(match, key, colon, value) => value.includes("={{") ? match : `${key}${colon}"***"`
|
|
2770
|
+
);
|
|
2771
|
+
}
|
|
2772
|
+
function lineDiff(local, server) {
|
|
2773
|
+
const a = local.split("\n");
|
|
2774
|
+
const b = server.split("\n");
|
|
2775
|
+
if (a.length * b.length > 1e6) {
|
|
2776
|
+
const lines = a.map((line) => `-${line}`);
|
|
2777
|
+
lines.push("---");
|
|
2778
|
+
lines.push(...b.map((line) => `+${line}`));
|
|
2779
|
+
return lines.join("\n");
|
|
2780
|
+
}
|
|
2781
|
+
const dp = Array.from(
|
|
2782
|
+
{ length: a.length + 1 },
|
|
2783
|
+
() => new Array(b.length + 1).fill(0)
|
|
2784
|
+
);
|
|
2785
|
+
for (let i2 = a.length - 1; i2 >= 0; i2 -= 1) {
|
|
2786
|
+
for (let j2 = b.length - 1; j2 >= 0; j2 -= 1) {
|
|
2787
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
const out = [];
|
|
2791
|
+
let i = 0;
|
|
2792
|
+
let j = 0;
|
|
2793
|
+
while (i < a.length && j < b.length) {
|
|
2794
|
+
if (a[i] === b[j]) {
|
|
2795
|
+
out.push(` ${a[i]}`);
|
|
2796
|
+
i += 1;
|
|
2797
|
+
j += 1;
|
|
2798
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
2799
|
+
out.push(`-${a[i]}`);
|
|
2800
|
+
i += 1;
|
|
2801
|
+
} else {
|
|
2802
|
+
out.push(`+${b[j]}`);
|
|
2803
|
+
j += 1;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
while (i < a.length) out.push(`-${a[i++]}`);
|
|
2807
|
+
while (j < b.length) out.push(`+${b[j++]}`);
|
|
2808
|
+
return out.join("\n");
|
|
2809
|
+
}
|
|
2810
|
+
function formatConflict(label, local, server) {
|
|
2811
|
+
return `${label}
|
|
2812
|
+
--- local
|
|
2813
|
+
+++ server
|
|
2814
|
+
${lineDiff(redactSecrets(local), redactSecrets(server))}`;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// src/cli/sync/store.ts
|
|
2818
|
+
import { createHash } from "crypto";
|
|
2819
|
+
import { lstat, mkdir, readFile, readdir, realpath, writeFile } from "fs/promises";
|
|
2820
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
2821
|
+
var PathEscapeError = class extends Error {
|
|
2822
|
+
constructor(target) {
|
|
2823
|
+
super(`Refusing to write outside the definitions folder: ${target}`);
|
|
2824
|
+
this.name = "PathEscapeError";
|
|
2825
|
+
}
|
|
2826
|
+
};
|
|
2827
|
+
function isOutsideDefinitionsDir(root, target) {
|
|
2828
|
+
const rootAbs = resolve(root);
|
|
2829
|
+
const abs = resolve(rootAbs, target);
|
|
2830
|
+
const rel = relative(rootAbs, abs);
|
|
2831
|
+
return rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel);
|
|
2832
|
+
}
|
|
2833
|
+
async function assertInsideDefinitionsDir(root, target) {
|
|
2834
|
+
const rootAbs = resolve(root);
|
|
2835
|
+
const abs = resolve(rootAbs, target);
|
|
2836
|
+
if (isOutsideDefinitionsDir(rootAbs, abs)) {
|
|
2837
|
+
throw new PathEscapeError(target);
|
|
2838
|
+
}
|
|
2839
|
+
let rootReal = rootAbs;
|
|
2840
|
+
try {
|
|
2841
|
+
rootReal = await realpath(rootAbs);
|
|
2842
|
+
} catch (error) {
|
|
2843
|
+
if (error.code !== "ENOENT") throw error;
|
|
2844
|
+
}
|
|
2845
|
+
const parts = relative(rootAbs, abs).split(sep).filter(Boolean);
|
|
2846
|
+
let cursor = rootAbs;
|
|
2847
|
+
for (const part of parts) {
|
|
2848
|
+
if (part === ".." || part === ".") throw new PathEscapeError(target);
|
|
2849
|
+
const next = resolve(cursor, part);
|
|
2850
|
+
let stats;
|
|
2851
|
+
try {
|
|
2852
|
+
stats = await lstat(next);
|
|
2853
|
+
} catch (error) {
|
|
2854
|
+
if (error.code === "ENOENT") return abs;
|
|
2855
|
+
throw error;
|
|
2856
|
+
}
|
|
2857
|
+
if (stats.isSymbolicLink()) {
|
|
2858
|
+
const real = await realpath(next);
|
|
2859
|
+
if (isOutsideDefinitionsDir(rootReal, real)) throw new PathEscapeError(target);
|
|
2860
|
+
cursor = real;
|
|
2861
|
+
continue;
|
|
2862
|
+
}
|
|
2863
|
+
cursor = next;
|
|
2864
|
+
}
|
|
2865
|
+
if (isOutsideDefinitionsDir(rootReal, cursor)) throw new PathEscapeError(target);
|
|
2866
|
+
return cursor;
|
|
2867
|
+
}
|
|
2868
|
+
async function writeGuarded(root, target, contents) {
|
|
2869
|
+
const abs = await assertInsideDefinitionsDir(root, target);
|
|
2870
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
2871
|
+
await writeFile(abs, contents);
|
|
2872
|
+
}
|
|
2873
|
+
function hashDefinition(json, instructions) {
|
|
2874
|
+
const hash = createHash("sha256");
|
|
2875
|
+
hash.update(json);
|
|
2876
|
+
if (instructions !== void 0) {
|
|
2877
|
+
hash.update("\0");
|
|
2878
|
+
hash.update(instructions);
|
|
2879
|
+
}
|
|
2880
|
+
return hash.digest("hex");
|
|
2881
|
+
}
|
|
2882
|
+
function deploymentKey(baseUrl, workspaceId) {
|
|
2883
|
+
return `${baseUrl.replace(/\/+$/, "")}#${workspaceId}`;
|
|
2884
|
+
}
|
|
2885
|
+
function emptyLock() {
|
|
2886
|
+
return { version: 1, deployments: {} };
|
|
2887
|
+
}
|
|
2888
|
+
async function readLock(root) {
|
|
2889
|
+
try {
|
|
2890
|
+
const text = await readFile(resolve(root, "lunnoa.lock.json"), "utf8");
|
|
2891
|
+
const parsed = JSON.parse(text);
|
|
2892
|
+
if (!parsed || parsed.version !== 1 || !parsed.deployments || typeof parsed.deployments !== "object") {
|
|
2893
|
+
throw new Error("lunnoa.lock.json is not a version 1 lock file.");
|
|
2894
|
+
}
|
|
2895
|
+
return parsed;
|
|
2896
|
+
} catch (error) {
|
|
2897
|
+
if (error.code === "ENOENT") return emptyLock();
|
|
2898
|
+
throw error;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
async function writeLock(root, lock) {
|
|
2902
|
+
await writeGuarded(root, "lunnoa.lock.json", `${JSON.stringify(lock, null, 2)}
|
|
2903
|
+
`);
|
|
2904
|
+
}
|
|
2905
|
+
function lockBucket(lock, key) {
|
|
2906
|
+
const existing = lock.deployments[key] ?? { agents: {}, workflows: {} };
|
|
2907
|
+
existing.agents ??= {};
|
|
2908
|
+
existing.workflows ??= {};
|
|
2909
|
+
lock.deployments[key] = existing;
|
|
2910
|
+
return existing;
|
|
2911
|
+
}
|
|
2912
|
+
function schemaRelativePath(jsonAbsolutePath, kind, projectRoot) {
|
|
2913
|
+
const target = resolve(
|
|
2914
|
+
projectRoot,
|
|
2915
|
+
"node_modules",
|
|
2916
|
+
"@lunnoa",
|
|
2917
|
+
"client",
|
|
2918
|
+
"schemas",
|
|
2919
|
+
`${kind}.schema.json`
|
|
2920
|
+
);
|
|
2921
|
+
let rel = relative(dirname(jsonAbsolutePath), target).split(sep).join("/");
|
|
2922
|
+
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
2923
|
+
return rel;
|
|
2924
|
+
}
|
|
2925
|
+
function serializeDefinition(definition, schema) {
|
|
2926
|
+
const body = {};
|
|
2927
|
+
for (const [key, value] of Object.entries(definition)) {
|
|
2928
|
+
if (key === "$schema") continue;
|
|
2929
|
+
body[key] = value;
|
|
2930
|
+
}
|
|
2931
|
+
return `${JSON.stringify({ $schema: schema, ...body }, null, 2)}
|
|
2932
|
+
`;
|
|
2933
|
+
}
|
|
2934
|
+
function renderAgentFiles(definition, schema) {
|
|
2935
|
+
if (typeof definition.instructions !== "string") {
|
|
2936
|
+
throw new Error(
|
|
2937
|
+
"Cannot write an agent definition whose instructions are still a file reference."
|
|
2938
|
+
);
|
|
2939
|
+
}
|
|
2940
|
+
const instructionsFile = `${definition.slug}.instructions.md`;
|
|
2941
|
+
const json = serializeDefinition(
|
|
2942
|
+
{ ...definition, instructions: { file: instructionsFile } },
|
|
2943
|
+
schema
|
|
2944
|
+
);
|
|
2945
|
+
return { json, instructions: definition.instructions, instructionsFile };
|
|
2946
|
+
}
|
|
2947
|
+
function renderWorkflowFile(definition, schema) {
|
|
2948
|
+
return serializeDefinition({ ...definition }, schema);
|
|
2949
|
+
}
|
|
2950
|
+
async function writeAgentDefinition(root, projectRoot, definition) {
|
|
2951
|
+
const jsonRel = join("agents", `${definition.slug}.json`);
|
|
2952
|
+
const schema = schemaRelativePath(resolve(root, jsonRel), "agent", projectRoot);
|
|
2953
|
+
const rendered = renderAgentFiles(definition, schema);
|
|
2954
|
+
await writeGuarded(root, jsonRel, rendered.json);
|
|
2955
|
+
await writeGuarded(root, join("agents", rendered.instructionsFile), rendered.instructions);
|
|
2956
|
+
return hashDefinition(rendered.json, rendered.instructions);
|
|
2957
|
+
}
|
|
2958
|
+
async function writeWorkflowDefinition(root, projectRoot, definition) {
|
|
2959
|
+
const jsonRel = join("workflows", `${definition.slug}.json`);
|
|
2960
|
+
const schema = schemaRelativePath(resolve(root, jsonRel), "workflow", projectRoot);
|
|
2961
|
+
const json = renderWorkflowFile(definition, schema);
|
|
2962
|
+
await writeGuarded(root, jsonRel, json);
|
|
2963
|
+
return hashDefinition(json);
|
|
2964
|
+
}
|
|
2965
|
+
async function readInside(root, rel) {
|
|
2966
|
+
const lexical = resolve(root, rel);
|
|
2967
|
+
if (isOutsideDefinitionsDir(root, lexical)) throw new PathEscapeError(rel);
|
|
2968
|
+
try {
|
|
2969
|
+
const abs = await assertInsideDefinitionsDir(root, rel);
|
|
2970
|
+
return await readFile(abs, "utf8");
|
|
2971
|
+
} catch (error) {
|
|
2972
|
+
if (error instanceof PathEscapeError) throw error;
|
|
2973
|
+
if (error.code === "ENOENT") return void 0;
|
|
2974
|
+
throw error;
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
async function hashOnDisk(root, kind, slug) {
|
|
2978
|
+
if (kind === "workflow") {
|
|
2979
|
+
const text = await readInside(root, join("workflows", `${slug}.json`));
|
|
2980
|
+
return text === void 0 ? void 0 : hashDefinition(text);
|
|
2981
|
+
}
|
|
2982
|
+
const json = await readInside(root, join("agents", `${slug}.json`));
|
|
2983
|
+
if (json === void 0) return void 0;
|
|
2984
|
+
const parsed = JSON.parse(json);
|
|
2985
|
+
if (parsed.instructions && typeof parsed.instructions === "object" && typeof parsed.instructions.file === "string") {
|
|
2986
|
+
const instructions = await readInside(root, join("agents", parsed.instructions.file));
|
|
2987
|
+
return hashDefinition(json, instructions ?? "");
|
|
2988
|
+
}
|
|
2989
|
+
return hashDefinition(json);
|
|
2990
|
+
}
|
|
2991
|
+
async function listDefinitionFiles(root) {
|
|
2992
|
+
const out = [];
|
|
2993
|
+
for (const kind of ["workflow", "agent"]) {
|
|
2994
|
+
const folder = kind === "agent" ? "agents" : "workflows";
|
|
2995
|
+
let names;
|
|
2996
|
+
try {
|
|
2997
|
+
names = await readdir(resolve(root, folder));
|
|
2998
|
+
} catch (error) {
|
|
2999
|
+
if (error.code === "ENOENT") continue;
|
|
3000
|
+
throw error;
|
|
3001
|
+
}
|
|
3002
|
+
for (const name of names.sort()) {
|
|
3003
|
+
if (!name.endsWith(".json")) continue;
|
|
3004
|
+
out.push({
|
|
3005
|
+
kind,
|
|
3006
|
+
slug: name.slice(0, -".json".length),
|
|
3007
|
+
path: resolve(root, folder, name)
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
return out;
|
|
3012
|
+
}
|
|
3013
|
+
async function readLocalText(root, kind, slug) {
|
|
3014
|
+
const folder = kind === "agent" ? "agents" : "workflows";
|
|
3015
|
+
const json = await readInside(root, join(folder, `${slug}.json`));
|
|
3016
|
+
if (json === void 0) return void 0;
|
|
3017
|
+
if (kind === "workflow") return json;
|
|
3018
|
+
const parsed = JSON.parse(json);
|
|
3019
|
+
if (parsed.instructions && typeof parsed.instructions === "object" && typeof parsed.instructions.file === "string") {
|
|
3020
|
+
const md = await readInside(root, join("agents", parsed.instructions.file));
|
|
3021
|
+
return `${json}--- instructions ---
|
|
3022
|
+
${md ?? ""}`;
|
|
3023
|
+
}
|
|
3024
|
+
return json;
|
|
3025
|
+
}
|
|
3026
|
+
async function loadDefinitionForPush(root, filePath) {
|
|
3027
|
+
let text;
|
|
3028
|
+
try {
|
|
3029
|
+
text = await readFile(filePath, "utf8");
|
|
3030
|
+
} catch (error) {
|
|
3031
|
+
if (error.code === "ENOENT") {
|
|
3032
|
+
throw new Error(`Definition file not found: ${filePath}`);
|
|
3033
|
+
}
|
|
3034
|
+
throw error;
|
|
3035
|
+
}
|
|
3036
|
+
let parsed;
|
|
3037
|
+
try {
|
|
3038
|
+
parsed = JSON.parse(text);
|
|
3039
|
+
} catch {
|
|
3040
|
+
throw new Error(`Invalid JSON in ${filePath}`);
|
|
3041
|
+
}
|
|
3042
|
+
if (parsed.kind === "agent") {
|
|
3043
|
+
const agent = acceptAgentFile(parsed);
|
|
3044
|
+
if (typeof agent.instructions === "object") {
|
|
3045
|
+
const mdPath = resolve(dirname(resolve(filePath)), agent.instructions.file);
|
|
3046
|
+
const boundary = isOutsideDefinitionsDir(root, resolve(filePath)) ? dirname(resolve(filePath)) : root;
|
|
3047
|
+
await assertInsideDefinitionsDir(boundary, mdPath);
|
|
3048
|
+
const instructions = await readFile(mdPath, "utf8");
|
|
3049
|
+
return { kind: "agent", definition: { ...agent, instructions } };
|
|
3050
|
+
}
|
|
3051
|
+
return { kind: "agent", definition: agent };
|
|
3052
|
+
}
|
|
3053
|
+
if (parsed.kind === "workflow" || parsed.format === "steps" || parsed.format === "graph") {
|
|
3054
|
+
if (parsed.format === "graph" || Array.isArray(parsed.nodes) && !parsed.steps) {
|
|
3055
|
+
return {
|
|
3056
|
+
kind: "workflow",
|
|
3057
|
+
definition: acceptWorkflowGraphFile(parsed)
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
return {
|
|
3061
|
+
kind: "workflow",
|
|
3062
|
+
definition: acceptWorkflowStepsFile(parsed)
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
throw new Error(`Definition JSON needs kind "agent" or "workflow": ${filePath}`);
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
// src/cli/sync/engine.ts
|
|
3069
|
+
var SyncConflictError = class extends Error {
|
|
3070
|
+
constructor(message) {
|
|
3071
|
+
super(message);
|
|
3072
|
+
this.name = "SyncConflictError";
|
|
3073
|
+
}
|
|
3074
|
+
};
|
|
3075
|
+
function folderOf(kind) {
|
|
3076
|
+
return kind === "agent" ? "agents" : "workflows";
|
|
3077
|
+
}
|
|
3078
|
+
function labelOf(kind, slug) {
|
|
3079
|
+
return `${folderOf(kind)}/${slug}.json`;
|
|
3080
|
+
}
|
|
3081
|
+
async function fetchDefinition(client, kind, slug) {
|
|
3082
|
+
try {
|
|
3083
|
+
const body = kind === "agent" ? await client.agents.getDefinition(slug) : await client.workflows.getDefinition(slug);
|
|
3084
|
+
return {
|
|
3085
|
+
kind,
|
|
3086
|
+
id: body.id,
|
|
3087
|
+
slug: body.definition?.slug || body.slug,
|
|
3088
|
+
revision: body.revision,
|
|
3089
|
+
definition: body.definition
|
|
3090
|
+
};
|
|
3091
|
+
} catch (error) {
|
|
3092
|
+
if (error instanceof LunnoaApiError && error.isNotFound) return null;
|
|
3093
|
+
throw error;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
function warnSecrets(kind, slug, definition) {
|
|
3097
|
+
for (const field of findSecretFields(definition)) {
|
|
3098
|
+
console.error(
|
|
3099
|
+
`Warning: ${labelOf(kind, slug)} field "${field}" looks like a secret. Definitions are committed; keep secrets in Lunnoa connections.`
|
|
3100
|
+
);
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
function serverText(root, projectRoot, remote) {
|
|
3104
|
+
const rel = labelOf(remote.kind, remote.slug);
|
|
3105
|
+
if (remote.kind === "agent") {
|
|
3106
|
+
const schema2 = schemaRelativePath(resolve2(root, rel), "agent", projectRoot);
|
|
3107
|
+
const rendered = renderAgentFiles(remote.definition, schema2);
|
|
3108
|
+
return `${rendered.json}--- instructions ---
|
|
3109
|
+
${rendered.instructions}`;
|
|
3110
|
+
}
|
|
3111
|
+
const schema = schemaRelativePath(resolve2(root, rel), "workflow", projectRoot);
|
|
3112
|
+
return renderWorkflowFile(remote.definition, schema);
|
|
3113
|
+
}
|
|
3114
|
+
async function resolvePullTargets(client, lock, key, selector) {
|
|
3115
|
+
if (selector.scope === "tracked") {
|
|
3116
|
+
const dep = lock.deployments[key];
|
|
3117
|
+
return [
|
|
3118
|
+
...Object.keys(dep?.workflows ?? {}).sort().map((slug) => ({ kind: "workflow", slug })),
|
|
3119
|
+
...Object.keys(dep?.agents ?? {}).sort().map((slug) => ({ kind: "agent", slug }))
|
|
3120
|
+
];
|
|
3121
|
+
}
|
|
3122
|
+
if (selector.scope === "all") {
|
|
3123
|
+
const [workflows, agents] = await Promise.all([
|
|
3124
|
+
client.workflows.list({ expansion: ["slug"] }),
|
|
3125
|
+
client.agents.list({ expansion: ["slug"] })
|
|
3126
|
+
]);
|
|
3127
|
+
return [
|
|
3128
|
+
...workflows.filter((workflow2) => workflow2.slug).map((workflow2) => ({ kind: "workflow", slug: workflow2.slug })),
|
|
3129
|
+
...agents.filter((agent2) => agent2.slug).map((agent2) => ({ kind: "agent", slug: agent2.slug }))
|
|
3130
|
+
];
|
|
3131
|
+
}
|
|
3132
|
+
if (selector.kind) return [{ kind: selector.kind, slug: selector.slug }];
|
|
3133
|
+
const [agent, workflow] = await Promise.all([
|
|
3134
|
+
fetchDefinition(client, "agent", selector.slug),
|
|
3135
|
+
fetchDefinition(client, "workflow", selector.slug)
|
|
3136
|
+
]);
|
|
3137
|
+
if (agent && workflow) {
|
|
3138
|
+
throw new Error(
|
|
3139
|
+
`Slug "${selector.slug}" matches an agent and a workflow. Use agents/${selector.slug} or workflows/${selector.slug}.`
|
|
3140
|
+
);
|
|
3141
|
+
}
|
|
3142
|
+
if (agent) return [{ kind: "agent", slug: selector.slug }];
|
|
3143
|
+
if (workflow) return [{ kind: "workflow", slug: selector.slug }];
|
|
3144
|
+
throw new Error(`No agent or workflow with slug "${selector.slug}".`);
|
|
3145
|
+
}
|
|
3146
|
+
async function pullResources(request, selector) {
|
|
3147
|
+
const context = await request.client.codeSync.context();
|
|
3148
|
+
const key = deploymentKey(request.client.http.baseUrl, context.workspaceId);
|
|
3149
|
+
const lock = await readLock(request.definitionsDir);
|
|
3150
|
+
const targets = await resolvePullTargets(request.client, lock, key, selector);
|
|
3151
|
+
const bucket = lockBucket(lock, key);
|
|
3152
|
+
const writes = [];
|
|
3153
|
+
const skipped = [];
|
|
3154
|
+
const conflicts = [];
|
|
3155
|
+
for (const target of targets) {
|
|
3156
|
+
const remote = await fetchDefinition(request.client, target.kind, target.slug);
|
|
3157
|
+
if (!remote) {
|
|
3158
|
+
throw new Error(
|
|
3159
|
+
`${target.kind === "agent" ? "Agent" : "Workflow"} "${target.slug}" is missing in Lunnoa.`
|
|
3160
|
+
);
|
|
3161
|
+
}
|
|
3162
|
+
const entry = bucket[folderOf(target.kind)][remote.slug];
|
|
3163
|
+
const hash = await hashOnDisk(request.definitionsDir, target.kind, remote.slug);
|
|
3164
|
+
if (entry && entry.revision === remote.revision) {
|
|
3165
|
+
skipped.push(labelOf(target.kind, remote.slug));
|
|
3166
|
+
continue;
|
|
3167
|
+
}
|
|
3168
|
+
const localChanged = hash !== void 0 && (!entry || hash !== entry.fileHash);
|
|
3169
|
+
const serverChanged = !entry || entry.revision !== remote.revision;
|
|
3170
|
+
if (localChanged && serverChanged && !request.force) {
|
|
3171
|
+
warnSecrets(target.kind, remote.slug, remote.definition);
|
|
3172
|
+
const local = await readLocalText(request.definitionsDir, target.kind, remote.slug) ?? "";
|
|
3173
|
+
conflicts.push(
|
|
3174
|
+
formatConflict(
|
|
3175
|
+
labelOf(target.kind, remote.slug),
|
|
3176
|
+
local,
|
|
3177
|
+
serverText(request.definitionsDir, request.projectRoot, remote)
|
|
3178
|
+
)
|
|
3179
|
+
);
|
|
3180
|
+
continue;
|
|
3181
|
+
}
|
|
3182
|
+
writes.push(remote);
|
|
3183
|
+
}
|
|
3184
|
+
if (conflicts.length) {
|
|
3185
|
+
throw new SyncConflictError(
|
|
3186
|
+
`Local and server both changed. Run pull --force to overwrite.
|
|
3187
|
+
|
|
3188
|
+
${conflicts.join("\n\n")}`
|
|
3189
|
+
);
|
|
3190
|
+
}
|
|
3191
|
+
const written = [];
|
|
3192
|
+
for (const remote of writes) {
|
|
3193
|
+
warnSecrets(remote.kind, remote.slug, remote.definition);
|
|
3194
|
+
const fileHash = remote.kind === "agent" ? await writeAgentDefinition(
|
|
3195
|
+
request.definitionsDir,
|
|
3196
|
+
request.projectRoot,
|
|
3197
|
+
remote.definition
|
|
3198
|
+
) : await writeWorkflowDefinition(
|
|
3199
|
+
request.definitionsDir,
|
|
3200
|
+
request.projectRoot,
|
|
3201
|
+
remote.definition
|
|
3202
|
+
);
|
|
3203
|
+
bucket[folderOf(remote.kind)][remote.slug] = {
|
|
3204
|
+
id: remote.id,
|
|
3205
|
+
revision: remote.revision,
|
|
3206
|
+
fileHash
|
|
3207
|
+
};
|
|
3208
|
+
written.push(labelOf(remote.kind, remote.slug));
|
|
3209
|
+
}
|
|
3210
|
+
if (written.length) await writeLock(request.definitionsDir, lock);
|
|
3211
|
+
return { written, skipped };
|
|
3212
|
+
}
|
|
3213
|
+
async function resolvePushFiles(root, selector) {
|
|
3214
|
+
if (selector.scope === "all") {
|
|
3215
|
+
return (await listDefinitionFiles(root)).map((file) => ({
|
|
3216
|
+
kind: file.kind,
|
|
3217
|
+
path: file.path
|
|
3218
|
+
}));
|
|
3219
|
+
}
|
|
3220
|
+
if (selector.file) return [{ kind: selector.kind, path: resolve2(selector.file) }];
|
|
3221
|
+
if (selector.kind && selector.slug) {
|
|
3222
|
+
return [
|
|
3223
|
+
{
|
|
3224
|
+
kind: selector.kind,
|
|
3225
|
+
path: resolve2(root, folderOf(selector.kind), `${selector.slug}.json`)
|
|
3226
|
+
}
|
|
3227
|
+
];
|
|
3228
|
+
}
|
|
3229
|
+
if (!selector.slug) throw new Error("Missing definition to push.");
|
|
3230
|
+
const found = (await listDefinitionFiles(root)).filter((file) => file.slug === selector.slug);
|
|
3231
|
+
if (found.length === 0) throw new Error(`No definition file for "${selector.slug}".`);
|
|
3232
|
+
if (found.length > 1) {
|
|
3233
|
+
throw new Error(
|
|
3234
|
+
`Slug "${selector.slug}" matches an agent and a workflow. Use agents/${selector.slug} or workflows/${selector.slug}.`
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
return [{ kind: found[0].kind, path: found[0].path }];
|
|
3238
|
+
}
|
|
3239
|
+
async function pushResources(request, selector) {
|
|
3240
|
+
const context = await request.client.codeSync.context();
|
|
3241
|
+
const key = deploymentKey(request.client.http.baseUrl, context.workspaceId);
|
|
3242
|
+
const lock = await readLock(request.definitionsDir);
|
|
3243
|
+
const bucket = lockBucket(lock, key);
|
|
3244
|
+
const files = await resolvePushFiles(request.definitionsDir, selector);
|
|
3245
|
+
const pushed = [];
|
|
3246
|
+
for (const file of files) {
|
|
3247
|
+
const loaded = await loadDefinitionForPush(request.definitionsDir, file.path);
|
|
3248
|
+
if (file.kind && loaded.kind !== file.kind) {
|
|
3249
|
+
throw new Error(`Expected a ${file.kind} definition, found ${loaded.kind}.`);
|
|
3250
|
+
}
|
|
3251
|
+
const slug = loaded.definition.slug;
|
|
3252
|
+
const entry = bucket[folderOf(loaded.kind)][slug];
|
|
3253
|
+
const options = {
|
|
3254
|
+
codeSource: "synced",
|
|
3255
|
+
...entry ? { baseRevision: entry.revision } : {},
|
|
3256
|
+
...request.force ? { force: true } : {}
|
|
3257
|
+
};
|
|
3258
|
+
let id = "";
|
|
3259
|
+
let revision = "";
|
|
3260
|
+
let definition;
|
|
3261
|
+
try {
|
|
3262
|
+
if (loaded.kind === "workflow") {
|
|
3263
|
+
const result = await request.client.workflows.upsertBySlug(
|
|
3264
|
+
loaded.definition,
|
|
3265
|
+
options
|
|
3266
|
+
);
|
|
3267
|
+
id = result.id;
|
|
3268
|
+
revision = result.revision;
|
|
3269
|
+
definition = result.definition;
|
|
3270
|
+
} else {
|
|
3271
|
+
const result = await request.client.agents.upsertBySlug(
|
|
3272
|
+
loaded.definition,
|
|
3273
|
+
options
|
|
3274
|
+
);
|
|
3275
|
+
id = result.id;
|
|
3276
|
+
revision = result.revision;
|
|
3277
|
+
definition = result.definition;
|
|
3278
|
+
}
|
|
3279
|
+
} catch (error) {
|
|
3280
|
+
if (error instanceof LunnoaApiError && error.status === 409) {
|
|
3281
|
+
throw new Error(publicApiMessage(error));
|
|
3282
|
+
}
|
|
3283
|
+
throw error;
|
|
3284
|
+
}
|
|
3285
|
+
const fileHash = loaded.kind === "agent" ? await writeAgentDefinition(
|
|
3286
|
+
request.definitionsDir,
|
|
3287
|
+
request.projectRoot,
|
|
3288
|
+
definition
|
|
3289
|
+
) : await writeWorkflowDefinition(
|
|
3290
|
+
request.definitionsDir,
|
|
3291
|
+
request.projectRoot,
|
|
3292
|
+
definition
|
|
3293
|
+
);
|
|
3294
|
+
bucket[folderOf(loaded.kind)][definition.slug || slug] = { id, revision, fileHash };
|
|
3295
|
+
await writeLock(request.definitionsDir, lock);
|
|
3296
|
+
pushed.push(labelOf(loaded.kind, definition.slug || slug));
|
|
3297
|
+
}
|
|
3298
|
+
return { pushed };
|
|
3299
|
+
}
|
|
3300
|
+
function localState(exists, hash, entry) {
|
|
3301
|
+
if (!exists) return "missing";
|
|
3302
|
+
if (!entry) return "new";
|
|
3303
|
+
return hash === entry.fileHash ? "unchanged" : "modified";
|
|
3304
|
+
}
|
|
3305
|
+
function remoteState(remote, entry) {
|
|
3306
|
+
if (!remote) return "missing in Lunnoa";
|
|
3307
|
+
if (entry && entry.revision === remote.revision) return "unchanged";
|
|
3308
|
+
return "changed in UI";
|
|
3309
|
+
}
|
|
3310
|
+
function isConflict(local, remote) {
|
|
3311
|
+
if (local === "new" && remote === "missing in Lunnoa") return false;
|
|
3312
|
+
if (local === "missing" && remote === "missing in Lunnoa") return false;
|
|
3313
|
+
return local !== "unchanged" && remote !== "unchanged";
|
|
3314
|
+
}
|
|
3315
|
+
async function statusResources(request) {
|
|
3316
|
+
const context = await request.client.codeSync.context();
|
|
3317
|
+
const key = deploymentKey(request.client.http.baseUrl, context.workspaceId);
|
|
3318
|
+
const lock = await readLock(request.definitionsDir);
|
|
3319
|
+
const dep = lock.deployments[key];
|
|
3320
|
+
const items = /* @__PURE__ */ new Map();
|
|
3321
|
+
for (const slug of Object.keys(dep?.workflows ?? {})) {
|
|
3322
|
+
items.set(`workflow:${slug}`, { kind: "workflow", slug });
|
|
3323
|
+
}
|
|
3324
|
+
for (const slug of Object.keys(dep?.agents ?? {})) {
|
|
3325
|
+
items.set(`agent:${slug}`, { kind: "agent", slug });
|
|
3326
|
+
}
|
|
3327
|
+
for (const file of await listDefinitionFiles(request.definitionsDir)) {
|
|
3328
|
+
items.set(`${file.kind}:${file.slug}`, { kind: file.kind, slug: file.slug });
|
|
3329
|
+
}
|
|
3330
|
+
const ordered = [...items.values()].sort((a, b) => {
|
|
3331
|
+
if (a.kind !== b.kind) return a.kind === "workflow" ? -1 : 1;
|
|
3332
|
+
return a.slug.localeCompare(b.slug);
|
|
3333
|
+
});
|
|
3334
|
+
const rows = [];
|
|
3335
|
+
for (const item of ordered) {
|
|
3336
|
+
const remote = await fetchDefinition(request.client, item.kind, item.slug);
|
|
3337
|
+
const hash = await hashOnDisk(request.definitionsDir, item.kind, item.slug);
|
|
3338
|
+
const entry = dep?.[folderOf(item.kind)]?.[item.slug];
|
|
3339
|
+
const local = localState(hash !== void 0, hash, entry);
|
|
3340
|
+
const remoteLabel = remoteState(remote, entry);
|
|
3341
|
+
rows.push({
|
|
3342
|
+
kind: item.kind,
|
|
3343
|
+
slug: item.slug,
|
|
3344
|
+
local,
|
|
3345
|
+
remote: remoteLabel,
|
|
3346
|
+
conflict: isConflict(local, remoteLabel)
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
return rows;
|
|
3350
|
+
}
|
|
3351
|
+
|
|
2324
3352
|
// src/cli/catalog.ts
|
|
2325
3353
|
function asRecord(value) {
|
|
2326
3354
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
@@ -2567,44 +3595,6 @@ function actionDetail(action) {
|
|
|
2567
3595
|
};
|
|
2568
3596
|
}
|
|
2569
3597
|
|
|
2570
|
-
// src/cli/print.ts
|
|
2571
|
-
function printJson(value) {
|
|
2572
|
-
console.log(JSON.stringify(value, null, 2));
|
|
2573
|
-
}
|
|
2574
|
-
function formatTable(rows, columns) {
|
|
2575
|
-
if (rows.length === 0) {
|
|
2576
|
-
return "No results.";
|
|
2577
|
-
}
|
|
2578
|
-
const widths = columns.map((col) => {
|
|
2579
|
-
let width = col.header.length;
|
|
2580
|
-
for (const row of rows) {
|
|
2581
|
-
width = Math.max(width, stringifyCell(row[col.key]).length);
|
|
2582
|
-
}
|
|
2583
|
-
return width;
|
|
2584
|
-
});
|
|
2585
|
-
const header = columns.map((col, i) => pad(col.header, widths[i])).join(" ");
|
|
2586
|
-
const divider = widths.map((w) => "-".repeat(w)).join(" ");
|
|
2587
|
-
const body = rows.map(
|
|
2588
|
-
(row) => columns.map((col, i) => pad(stringifyCell(row[col.key]), widths[i])).join(" ")
|
|
2589
|
-
);
|
|
2590
|
-
return [header, divider, ...body].join("\n");
|
|
2591
|
-
}
|
|
2592
|
-
function printTable(rows, columns) {
|
|
2593
|
-
console.log(formatTable(rows, columns));
|
|
2594
|
-
}
|
|
2595
|
-
function stringifyCell(value) {
|
|
2596
|
-
if (value === null || value === void 0) {
|
|
2597
|
-
return "";
|
|
2598
|
-
}
|
|
2599
|
-
if (typeof value === "boolean") {
|
|
2600
|
-
return value ? "yes" : "no";
|
|
2601
|
-
}
|
|
2602
|
-
return String(value);
|
|
2603
|
-
}
|
|
2604
|
-
function pad(value, width) {
|
|
2605
|
-
return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
|
|
2606
|
-
}
|
|
2607
|
-
|
|
2608
3598
|
// src/cli/discover.ts
|
|
2609
3599
|
function requireTarget(args, label) {
|
|
2610
3600
|
const value = args.path?.trim();
|
|
@@ -2845,13 +3835,13 @@ async function runConnectionsGet(client, args) {
|
|
|
2845
3835
|
}
|
|
2846
3836
|
async function runAgentsList(client, args) {
|
|
2847
3837
|
const agents = await client.agents.list({
|
|
2848
|
-
expansion: ["description", "slug", "
|
|
3838
|
+
expansion: ["description", "slug", "codeSource"]
|
|
2849
3839
|
});
|
|
2850
3840
|
const rows = agents.map((agent) => ({
|
|
2851
3841
|
id: agent.id,
|
|
2852
3842
|
name: agent.name,
|
|
2853
3843
|
slug: agent.slug ?? "",
|
|
2854
|
-
|
|
3844
|
+
codeSource: agent.codeSource ?? "none",
|
|
2855
3845
|
description: agent.description ?? ""
|
|
2856
3846
|
}));
|
|
2857
3847
|
if (args.json) {
|
|
@@ -2862,19 +3852,19 @@ async function runAgentsList(client, args) {
|
|
|
2862
3852
|
{ key: "id", header: "ID" },
|
|
2863
3853
|
{ key: "slug", header: "SLUG" },
|
|
2864
3854
|
{ key: "name", header: "NAME" },
|
|
2865
|
-
{ key: "
|
|
3855
|
+
{ key: "codeSource", header: "SOURCE" }
|
|
2866
3856
|
]);
|
|
2867
3857
|
}
|
|
2868
3858
|
async function runWorkflowsList(client, args) {
|
|
2869
3859
|
const workflows = await client.workflows.list({
|
|
2870
|
-
expansion: ["description", "isActive", "slug", "
|
|
3860
|
+
expansion: ["description", "isActive", "slug", "codeSource"]
|
|
2871
3861
|
});
|
|
2872
3862
|
const rows = workflows.map((workflow) => ({
|
|
2873
3863
|
id: workflow.id,
|
|
2874
3864
|
name: workflow.name,
|
|
2875
3865
|
slug: workflow.slug ?? "",
|
|
2876
3866
|
isActive: Boolean(workflow.isActive),
|
|
2877
|
-
|
|
3867
|
+
codeSource: workflow.codeSource ?? "none"
|
|
2878
3868
|
}));
|
|
2879
3869
|
if (args.json) {
|
|
2880
3870
|
printJson(rows);
|
|
@@ -2885,7 +3875,7 @@ async function runWorkflowsList(client, args) {
|
|
|
2885
3875
|
{ key: "slug", header: "SLUG" },
|
|
2886
3876
|
{ key: "name", header: "NAME" },
|
|
2887
3877
|
{ key: "isActive", header: "ACTIVE" },
|
|
2888
|
-
{ key: "
|
|
3878
|
+
{ key: "codeSource", header: "SOURCE" }
|
|
2889
3879
|
]);
|
|
2890
3880
|
}
|
|
2891
3881
|
async function runEntityTypesList(client, args) {
|
|
@@ -3361,7 +4351,7 @@ function typedAgentAccessor(client: LunnoaClient, agentId: string) {
|
|
|
3361
4351
|
|
|
3362
4352
|
// src/cli/load-definition.ts
|
|
3363
4353
|
import { pathToFileURL } from "url";
|
|
3364
|
-
import { resolve } from "path";
|
|
4354
|
+
import { resolve as resolve3 } from "path";
|
|
3365
4355
|
import { access } from "fs/promises";
|
|
3366
4356
|
import { constants as fsConstants } from "fs";
|
|
3367
4357
|
async function fileExists(path) {
|
|
@@ -3373,7 +4363,7 @@ async function fileExists(path) {
|
|
|
3373
4363
|
}
|
|
3374
4364
|
}
|
|
3375
4365
|
async function loadDefinitionModule(pathArg) {
|
|
3376
|
-
const absolute =
|
|
4366
|
+
const absolute = resolve3(process.cwd(), pathArg);
|
|
3377
4367
|
if (!await fileExists(absolute)) {
|
|
3378
4368
|
throw new Error(`Definition file not found: ${absolute}`);
|
|
3379
4369
|
}
|
|
@@ -3429,21 +4419,22 @@ async function loadOptionalConfig() {
|
|
|
3429
4419
|
"lunnoa.config.json"
|
|
3430
4420
|
];
|
|
3431
4421
|
for (const name of candidates) {
|
|
3432
|
-
const absolute =
|
|
4422
|
+
const absolute = resolve3(process.cwd(), name);
|
|
3433
4423
|
if (!await fileExists(absolute)) {
|
|
3434
4424
|
continue;
|
|
3435
4425
|
}
|
|
3436
4426
|
if (name.endsWith(".json")) {
|
|
3437
|
-
const { readFile:
|
|
3438
|
-
const raw = await
|
|
4427
|
+
const { readFile: readFile3 } = await import("fs/promises");
|
|
4428
|
+
const raw = await readFile3(absolute, "utf8");
|
|
3439
4429
|
const cfg2 = JSON.parse(raw);
|
|
3440
|
-
return { url: cfg2.url, apiKey: cfg2.apiKey };
|
|
4430
|
+
return { url: cfg2.url, apiKey: cfg2.apiKey, definitionsDir: cfg2.definitionsDir };
|
|
3441
4431
|
}
|
|
3442
4432
|
const mod = await import(pathToFileURL(absolute).href);
|
|
3443
4433
|
const cfg = mod.default ?? mod;
|
|
3444
4434
|
return {
|
|
3445
4435
|
url: cfg.url,
|
|
3446
|
-
apiKey: cfg.apiKey
|
|
4436
|
+
apiKey: cfg.apiKey,
|
|
4437
|
+
definitionsDir: cfg.definitionsDir
|
|
3447
4438
|
};
|
|
3448
4439
|
}
|
|
3449
4440
|
return {};
|
|
@@ -3451,8 +4442,8 @@ async function loadOptionalConfig() {
|
|
|
3451
4442
|
|
|
3452
4443
|
// src/cli/init.ts
|
|
3453
4444
|
import { spawn } from "child_process";
|
|
3454
|
-
import { cp, mkdir, readdir, readFile, stat, writeFile } from "fs/promises";
|
|
3455
|
-
import { basename, join, resolve as
|
|
4445
|
+
import { cp, mkdir as mkdir2, readdir as readdir2, readFile as readFile2, stat, writeFile as writeFile2 } from "fs/promises";
|
|
4446
|
+
import { basename, join as join2, resolve as resolve4 } from "path";
|
|
3456
4447
|
import { createInterface } from "readline/promises";
|
|
3457
4448
|
var DEFAULT_TEMPLATE = "lunnoa-automate/lunnoa-starter";
|
|
3458
4449
|
var SKIP_LOCAL = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", ".env", ".claude", ".cursor"]);
|
|
@@ -3505,14 +4496,14 @@ async function runInit(args) {
|
|
|
3505
4496
|
if (!args.path) {
|
|
3506
4497
|
throw new Error("Missing project directory. Example: init my-app --url https://lunnoa.example");
|
|
3507
4498
|
}
|
|
3508
|
-
const dest =
|
|
3509
|
-
if ((await
|
|
4499
|
+
const dest = resolve4(process.cwd(), args.path);
|
|
4500
|
+
if ((await readdir2(dest).catch(() => [])).length > 0) {
|
|
3510
4501
|
throw new Error(`${dest} already exists and is not empty.`);
|
|
3511
4502
|
}
|
|
3512
|
-
await
|
|
4503
|
+
await mkdir2(dest, { recursive: true });
|
|
3513
4504
|
const template = args.template ?? DEFAULT_TEMPLATE;
|
|
3514
|
-
if (await isDirectory(
|
|
3515
|
-
await cp(
|
|
4505
|
+
if (await isDirectory(resolve4(template))) {
|
|
4506
|
+
await cp(resolve4(template), dest, {
|
|
3516
4507
|
recursive: true,
|
|
3517
4508
|
filter: (src) => !SKIP_LOCAL.has(basename(src))
|
|
3518
4509
|
});
|
|
@@ -3527,9 +4518,9 @@ async function runInit(args) {
|
|
|
3527
4518
|
apiKey ||= (await rl.question("API key lna_... (CLI only: discovery, codegen, deploy. Enter to skip): ")).trim();
|
|
3528
4519
|
rl.close();
|
|
3529
4520
|
}
|
|
3530
|
-
const example = await
|
|
3531
|
-
await
|
|
3532
|
-
|
|
4521
|
+
const example = await readFile2(join2(dest, ".env.example"), "utf8").catch(() => "");
|
|
4522
|
+
await writeFile2(
|
|
4523
|
+
join2(dest, ".env"),
|
|
3533
4524
|
setEnvValues(dropRetiredProjectEnv(example), {
|
|
3534
4525
|
LUNNOA_URL: url,
|
|
3535
4526
|
LUNNOA_API_KEY: apiKey
|
|
@@ -3551,18 +4542,18 @@ Next steps:`);
|
|
|
3551
4542
|
|
|
3552
4543
|
// src/cli/skills.ts
|
|
3553
4544
|
import { cp as cp2 } from "fs/promises";
|
|
3554
|
-
import { dirname, resolve as
|
|
4545
|
+
import { dirname as dirname2, resolve as resolve5 } from "path";
|
|
3555
4546
|
import { fileURLToPath } from "url";
|
|
3556
4547
|
var SKILL_TARGETS = [".claude/skills", ".cursor/skills"];
|
|
3557
|
-
var BUNDLED_SKILLS_DIR =
|
|
3558
|
-
|
|
4548
|
+
var BUNDLED_SKILLS_DIR = resolve5(
|
|
4549
|
+
dirname2(fileURLToPath(import.meta.url)),
|
|
3559
4550
|
"..",
|
|
3560
4551
|
"skills"
|
|
3561
4552
|
);
|
|
3562
4553
|
async function installSkills(projectDir, sourceDir = BUNDLED_SKILLS_DIR) {
|
|
3563
4554
|
const written = [];
|
|
3564
4555
|
for (const target of SKILL_TARGETS) {
|
|
3565
|
-
const dest =
|
|
4556
|
+
const dest = resolve5(projectDir, target);
|
|
3566
4557
|
await cp2(sourceDir, dest, { recursive: true, force: true });
|
|
3567
4558
|
written.push(dest);
|
|
3568
4559
|
}
|
|
@@ -3572,6 +4563,11 @@ async function installSkills(projectDir, sourceDir = BUNDLED_SKILLS_DIR) {
|
|
|
3572
4563
|
// src/cli/index.ts
|
|
3573
4564
|
async function bindClient(args) {
|
|
3574
4565
|
const fileConfig = await loadOptionalConfig();
|
|
4566
|
+
const projectRoot = process.cwd();
|
|
4567
|
+
const definitionsDir = resolve6(
|
|
4568
|
+
projectRoot,
|
|
4569
|
+
args.dir ?? fileConfig.definitionsDir ?? "lunnoa"
|
|
4570
|
+
);
|
|
3575
4571
|
const url = args.url ?? process.env.LUNNOA_URL ?? fileConfig.url;
|
|
3576
4572
|
const apiKey = args.apiKey ?? process.env.LUNNOA_API_KEY ?? fileConfig.apiKey;
|
|
3577
4573
|
const accessToken = args.accessToken;
|
|
@@ -3585,9 +4581,114 @@ async function bindClient(args) {
|
|
|
3585
4581
|
}
|
|
3586
4582
|
return {
|
|
3587
4583
|
client: new LunnoaClient({ baseUrl: url, apiKey, accessToken }),
|
|
3588
|
-
url
|
|
4584
|
+
url,
|
|
4585
|
+
definitionsDir,
|
|
4586
|
+
projectRoot
|
|
3589
4587
|
};
|
|
3590
4588
|
}
|
|
4589
|
+
function resourceSelector(path) {
|
|
4590
|
+
const normalized = path.replace(/\\/g, "/");
|
|
4591
|
+
const file = /^(?:.*\/)?(agents|workflows)\/([^/]+)\.json$/.exec(normalized);
|
|
4592
|
+
if (file) {
|
|
4593
|
+
return {
|
|
4594
|
+
kind: file[1] === "agents" ? "agent" : "workflow",
|
|
4595
|
+
slug: file[2],
|
|
4596
|
+
file: path
|
|
4597
|
+
};
|
|
4598
|
+
}
|
|
4599
|
+
const prefixed = /^(agents|workflows)\/([^/]+)$/.exec(normalized);
|
|
4600
|
+
if (prefixed) {
|
|
4601
|
+
return {
|
|
4602
|
+
kind: prefixed[1] === "agents" ? "agent" : "workflow",
|
|
4603
|
+
slug: prefixed[2]
|
|
4604
|
+
};
|
|
4605
|
+
}
|
|
4606
|
+
if (normalized.endsWith(".json") || normalized.includes("/")) {
|
|
4607
|
+
return { slug: "", file: path };
|
|
4608
|
+
}
|
|
4609
|
+
return { slug: normalized };
|
|
4610
|
+
}
|
|
4611
|
+
function pullSelector(args) {
|
|
4612
|
+
if (args.all && args.path) {
|
|
4613
|
+
throw new Error("pull --all does not take a slug.");
|
|
4614
|
+
}
|
|
4615
|
+
if (args.all || !args.path) {
|
|
4616
|
+
return args.all ? { scope: "all" } : { scope: "tracked" };
|
|
4617
|
+
}
|
|
4618
|
+
const parsed = resourceSelector(args.path);
|
|
4619
|
+
if (parsed.file && !parsed.kind) {
|
|
4620
|
+
throw new Error("Pass a slug, agents/<slug>, or workflows/<slug>.");
|
|
4621
|
+
}
|
|
4622
|
+
return { scope: "one", kind: parsed.kind, slug: parsed.slug };
|
|
4623
|
+
}
|
|
4624
|
+
function pushSelector(args, deploy) {
|
|
4625
|
+
if (deploy) return { scope: "one", kind: deploy.kind, file: deploy.file };
|
|
4626
|
+
if (args.all && args.path) {
|
|
4627
|
+
throw new Error("push --all does not take a path.");
|
|
4628
|
+
}
|
|
4629
|
+
if (!args.path) return { scope: "all" };
|
|
4630
|
+
const parsed = resourceSelector(args.path);
|
|
4631
|
+
if (parsed.file) return { scope: "one", kind: parsed.kind, file: parsed.file };
|
|
4632
|
+
return { scope: "one", kind: parsed.kind, slug: parsed.slug };
|
|
4633
|
+
}
|
|
4634
|
+
async function runPull(args) {
|
|
4635
|
+
const bound = await bindClient(args);
|
|
4636
|
+
const report = await pullResources(
|
|
4637
|
+
{
|
|
4638
|
+
client: bound.client,
|
|
4639
|
+
definitionsDir: bound.definitionsDir,
|
|
4640
|
+
projectRoot: bound.projectRoot,
|
|
4641
|
+
force: args.force
|
|
4642
|
+
},
|
|
4643
|
+
pullSelector(args)
|
|
4644
|
+
);
|
|
4645
|
+
if (report.written.length === 0) {
|
|
4646
|
+
console.log("Nothing to pull.");
|
|
4647
|
+
return;
|
|
4648
|
+
}
|
|
4649
|
+
for (const file of report.written) console.log(`Pulled ${file}`);
|
|
4650
|
+
}
|
|
4651
|
+
async function runPush(args, deploy) {
|
|
4652
|
+
const bound = await bindClient(args);
|
|
4653
|
+
const report = await pushResources(
|
|
4654
|
+
{
|
|
4655
|
+
client: bound.client,
|
|
4656
|
+
definitionsDir: bound.definitionsDir,
|
|
4657
|
+
projectRoot: bound.projectRoot,
|
|
4658
|
+
force: args.force
|
|
4659
|
+
},
|
|
4660
|
+
pushSelector(args, deploy)
|
|
4661
|
+
);
|
|
4662
|
+
for (const file of report.pushed) console.log(`Pushed ${file}`);
|
|
4663
|
+
}
|
|
4664
|
+
async function runStatus(args) {
|
|
4665
|
+
const bound = await bindClient(args);
|
|
4666
|
+
const rows = await statusResources({
|
|
4667
|
+
client: bound.client,
|
|
4668
|
+
definitionsDir: bound.definitionsDir,
|
|
4669
|
+
projectRoot: bound.projectRoot
|
|
4670
|
+
});
|
|
4671
|
+
if (args.json) {
|
|
4672
|
+
printJson(rows);
|
|
4673
|
+
return;
|
|
4674
|
+
}
|
|
4675
|
+
printTable(
|
|
4676
|
+
rows.map((row) => ({
|
|
4677
|
+
kind: row.kind,
|
|
4678
|
+
slug: row.slug,
|
|
4679
|
+
local: row.local,
|
|
4680
|
+
remote: row.remote,
|
|
4681
|
+
conflict: row.conflict
|
|
4682
|
+
})),
|
|
4683
|
+
[
|
|
4684
|
+
{ key: "kind", header: "KIND" },
|
|
4685
|
+
{ key: "slug", header: "SLUG" },
|
|
4686
|
+
{ key: "local", header: "LOCAL" },
|
|
4687
|
+
{ key: "remote", header: "REMOTE" },
|
|
4688
|
+
{ key: "conflict", header: "CONFLICT" }
|
|
4689
|
+
]
|
|
4690
|
+
);
|
|
4691
|
+
}
|
|
3591
4692
|
async function fetchDefinitions(client, baseUrl) {
|
|
3592
4693
|
const entityTypes = await client.entityTypes.list({
|
|
3593
4694
|
expansion: ["attributeSchema", "stateSchema", "description"]
|
|
@@ -3622,16 +4723,20 @@ async function runCodegen(args) {
|
|
|
3622
4723
|
`Found ${definitions.entityTypes.length} entity type(s), ${definitions.workflows.length} workflow(s), ${definitions.agents.length} agent(s).`
|
|
3623
4724
|
);
|
|
3624
4725
|
const moduleSource = generateDeploymentModule(definitions);
|
|
3625
|
-
const outDir =
|
|
3626
|
-
await
|
|
3627
|
-
const outFile =
|
|
3628
|
-
await
|
|
4726
|
+
const outDir = resolve6(process.cwd(), args.out);
|
|
4727
|
+
await mkdir3(outDir, { recursive: true });
|
|
4728
|
+
const outFile = resolve6(outDir, "lunnoa.generated.ts");
|
|
4729
|
+
await writeFile3(outFile, moduleSource, "utf8");
|
|
3629
4730
|
console.log(`Wrote ${outFile}`);
|
|
3630
4731
|
}
|
|
3631
4732
|
async function deployWorkflow(args) {
|
|
3632
4733
|
if (!args.path) {
|
|
3633
4734
|
throw new Error("Missing definition path. Example: workflows deploy ./workflow.ts");
|
|
3634
4735
|
}
|
|
4736
|
+
if (args.path.endsWith(".json")) {
|
|
4737
|
+
await runPush(args, { kind: "workflow", file: args.path });
|
|
4738
|
+
return;
|
|
4739
|
+
}
|
|
3635
4740
|
const { client } = await bindClient(args);
|
|
3636
4741
|
const mod = await loadDefinitionModule(args.path);
|
|
3637
4742
|
const def = pickWorkflowDefinition(mod);
|
|
@@ -3644,6 +4749,10 @@ async function deployAgent(args) {
|
|
|
3644
4749
|
if (!args.path) {
|
|
3645
4750
|
throw new Error("Missing definition path. Example: agents deploy ./agent.ts");
|
|
3646
4751
|
}
|
|
4752
|
+
if (args.path.endsWith(".json")) {
|
|
4753
|
+
await runPush(args, { kind: "agent", file: args.path });
|
|
4754
|
+
return;
|
|
4755
|
+
}
|
|
3647
4756
|
const { client } = await bindClient(args);
|
|
3648
4757
|
const mod = await loadDefinitionModule(args.path);
|
|
3649
4758
|
const def = pickAgentDefinition(mod);
|
|
@@ -3785,8 +4894,12 @@ async function runDiscovery(args) {
|
|
|
3785
4894
|
}
|
|
3786
4895
|
return false;
|
|
3787
4896
|
}
|
|
3788
|
-
|
|
3789
|
-
|
|
4897
|
+
function reportCliError(error) {
|
|
4898
|
+
console.error(error instanceof Error ? error.message : error);
|
|
4899
|
+
process.exitCode = 1;
|
|
4900
|
+
}
|
|
4901
|
+
async function run(argv) {
|
|
4902
|
+
const args = parseArgs(argv);
|
|
3790
4903
|
try {
|
|
3791
4904
|
process.loadEnvFile();
|
|
3792
4905
|
} catch {
|
|
@@ -3800,7 +4913,7 @@ async function main() {
|
|
|
3800
4913
|
return;
|
|
3801
4914
|
}
|
|
3802
4915
|
if (args.command === "skills" && args.subcommand === "install") {
|
|
3803
|
-
const written = await installSkills(
|
|
4916
|
+
const written = await installSkills(resolve6(process.cwd(), args.path ?? "."));
|
|
3804
4917
|
for (const dest of written) {
|
|
3805
4918
|
console.log(`Installed Lunnoa skills into ${dest}`);
|
|
3806
4919
|
}
|
|
@@ -3810,6 +4923,18 @@ async function main() {
|
|
|
3810
4923
|
await runCodegen(args);
|
|
3811
4924
|
return;
|
|
3812
4925
|
}
|
|
4926
|
+
if (args.command === "pull") {
|
|
4927
|
+
await runPull(args);
|
|
4928
|
+
return;
|
|
4929
|
+
}
|
|
4930
|
+
if (args.command === "push") {
|
|
4931
|
+
await runPush(args);
|
|
4932
|
+
return;
|
|
4933
|
+
}
|
|
4934
|
+
if (args.command === "status") {
|
|
4935
|
+
await runStatus(args);
|
|
4936
|
+
return;
|
|
4937
|
+
}
|
|
3813
4938
|
if (await runDiscovery(args)) {
|
|
3814
4939
|
return;
|
|
3815
4940
|
}
|
|
@@ -3830,7 +4955,17 @@ async function main() {
|
|
|
3830
4955
|
console.log(USAGE);
|
|
3831
4956
|
process.exitCode = 1;
|
|
3832
4957
|
}
|
|
3833
|
-
main()
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
})
|
|
4958
|
+
async function main() {
|
|
4959
|
+
try {
|
|
4960
|
+
await run(process.argv.slice(2));
|
|
4961
|
+
} catch (error) {
|
|
4962
|
+
reportCliError(error);
|
|
4963
|
+
}
|
|
4964
|
+
}
|
|
4965
|
+
if (process.env.VITEST !== "true") {
|
|
4966
|
+
void main();
|
|
4967
|
+
}
|
|
4968
|
+
export {
|
|
4969
|
+
reportCliError,
|
|
4970
|
+
run
|
|
4971
|
+
};
|