@voltade/volty 0.1.23 → 0.1.25
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 +10 -0
- package/dist/volty.js +499 -403
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,16 @@ Open Claude Code anywhere and ask in plain language. The skill you installed in
|
|
|
48
48
|
|
|
49
49
|
Claude knows the safety rules: tests run on your workspace's Sandbox from a synthetic contact, whose replies are recorded in the thread but never sent, so nothing can reach a real contact. Claude cleans up after itself when it's done, and anything left over is removed after 24 hours.
|
|
50
50
|
|
|
51
|
+
## Working on a workspace's code in a folder
|
|
52
|
+
|
|
53
|
+
To edit a workspace's automations, pages, quick paths and inbox settings as files, start a folder with:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
volty init my-workspace
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
It pulls the workspace into the new folder and adds a `package.json`, a `.gitignore`, and a `tsconfig.json` that references `tsconfig.code.json` and `tsconfig.entry.json`, so your editor and Claude can check the code the way a push does with `npx tsc -b`, as the `@voltade/sdk` README explains. Run `npm install` in the folder, then open it in your editor. A file the folder already has is kept. `volty pull` brings in later changes from the workspace and `volty push` sends yours back.
|
|
60
|
+
|
|
51
61
|
## Upgrading
|
|
52
62
|
|
|
53
63
|
When a newer version exists, the tool prints a notice — Claude will see it and upgrade for you. To do it yourself:
|
package/dist/volty.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// bin/volty.ts
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
5
5
|
import { homedir as homedir3 } from "node:os";
|
|
6
|
-
import { join as
|
|
6
|
+
import { join as join7 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// src/api.ts
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
@@ -572,6 +572,202 @@ function renderResult(body, json) {
|
|
|
572
572
|
return text || "(no output)";
|
|
573
573
|
}
|
|
574
574
|
|
|
575
|
+
// src/init.ts
|
|
576
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
577
|
+
import { join as join4 } from "node:path";
|
|
578
|
+
|
|
579
|
+
// ../shared/projects/checker-options.ts
|
|
580
|
+
var checkerOptions = {
|
|
581
|
+
code: {
|
|
582
|
+
target: "ES2022",
|
|
583
|
+
lib: ["ES2022", "DOM", "DOM.Iterable"],
|
|
584
|
+
module: "ESNext",
|
|
585
|
+
moduleResolution: "bundler",
|
|
586
|
+
strict: true,
|
|
587
|
+
skipLibCheck: true,
|
|
588
|
+
esModuleInterop: true,
|
|
589
|
+
types: []
|
|
590
|
+
},
|
|
591
|
+
entry: {
|
|
592
|
+
strict: false,
|
|
593
|
+
jsx: "react-jsx"
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// ../shared/projects/paths.ts
|
|
598
|
+
var SEGMENT = "[A-Za-z0-9_][A-Za-z0-9._-]*";
|
|
599
|
+
var PROJECT_SEGMENT = new RegExp(`^${SEGMENT}$`);
|
|
600
|
+
var PROJECT_PATH = new RegExp(`^(?:${SEGMENT}/)*${SEGMENT}$`);
|
|
601
|
+
var PUSH_EXCLUDES = [
|
|
602
|
+
"node_modules/",
|
|
603
|
+
"bun.lock",
|
|
604
|
+
"bun.lockb",
|
|
605
|
+
"package-lock.json",
|
|
606
|
+
"yarn.lock",
|
|
607
|
+
"pnpm-lock.yaml",
|
|
608
|
+
"*.tsbuildinfo"
|
|
609
|
+
];
|
|
610
|
+
function pushes(name, isDirectory) {
|
|
611
|
+
if (!PROJECT_SEGMENT.test(name))
|
|
612
|
+
return false;
|
|
613
|
+
return !PUSH_EXCLUDES.some((pattern) => pattern.endsWith("/") ? isDirectory && name === pattern.slice(0, -1) : pattern.startsWith("*") ? name.endsWith(pattern.slice(1)) : name === pattern);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ../shared/projects/scaffold.ts
|
|
617
|
+
var SDK_VERSION_RANGE = "^0.1.0";
|
|
618
|
+
var SCAFFOLD_PATHS = [
|
|
619
|
+
"package.json",
|
|
620
|
+
"tsconfig.json",
|
|
621
|
+
"tsconfig.code.json",
|
|
622
|
+
"tsconfig.entry.json",
|
|
623
|
+
".gitignore"
|
|
624
|
+
];
|
|
625
|
+
var ENTRY_FILES = "pages/**/entry.tsx";
|
|
626
|
+
function options(name) {
|
|
627
|
+
return {
|
|
628
|
+
...checkerOptions.code,
|
|
629
|
+
composite: true,
|
|
630
|
+
noEmit: true,
|
|
631
|
+
tsBuildInfoFile: `./node_modules/.cache/volty/${name}.tsbuildinfo`,
|
|
632
|
+
jsx: checkerOptions.entry.jsx,
|
|
633
|
+
paths: { "~/*": ["./node_modules/@voltade/sdk/*"] }
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
var json = (value) => `${JSON.stringify(value, null, 2)}
|
|
637
|
+
`;
|
|
638
|
+
function scaffoldFiles() {
|
|
639
|
+
return {
|
|
640
|
+
"package.json": json({
|
|
641
|
+
name: "volty-project",
|
|
642
|
+
private: true,
|
|
643
|
+
type: "module",
|
|
644
|
+
devDependencies: {
|
|
645
|
+
"@types/react": "^19.0.0",
|
|
646
|
+
"@types/react-dom": "^19.0.0",
|
|
647
|
+
"@voltade/sdk": SDK_VERSION_RANGE,
|
|
648
|
+
typescript: "^5.9.0 || ^7.0.0"
|
|
649
|
+
}
|
|
650
|
+
}),
|
|
651
|
+
"tsconfig.json": json({
|
|
652
|
+
files: [],
|
|
653
|
+
references: [{ path: "./tsconfig.code.json" }, { path: "./tsconfig.entry.json" }]
|
|
654
|
+
}),
|
|
655
|
+
"tsconfig.code.json": json({
|
|
656
|
+
compilerOptions: { ...options("code"), allowUnreachableCode: false },
|
|
657
|
+
include: ["**/*.ts", "**/*.tsx"],
|
|
658
|
+
exclude: ["node_modules", ENTRY_FILES]
|
|
659
|
+
}),
|
|
660
|
+
"tsconfig.entry.json": json({
|
|
661
|
+
compilerOptions: { ...options("entry"), ...checkerOptions.entry, types: ["@voltade/sdk/globals"] },
|
|
662
|
+
include: [ENTRY_FILES]
|
|
663
|
+
}),
|
|
664
|
+
".gitignore": `${PUSH_EXCLUDES.join(`
|
|
665
|
+
`)}
|
|
666
|
+
`
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/pull.ts
|
|
671
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
672
|
+
import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
673
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
674
|
+
async function read(ctx, name, input = {}) {
|
|
675
|
+
const res = await runOp(ctx, { name }, input);
|
|
676
|
+
if (!res.ok)
|
|
677
|
+
throw new Error(`${name}: ${res.body?.error?.message ?? `HTTP ${res.status}`}`);
|
|
678
|
+
return res.body.structuredContent;
|
|
679
|
+
}
|
|
680
|
+
var sha256 = (text) => createHash2("sha256").update(text).digest("hex");
|
|
681
|
+
function readTargets(dir) {
|
|
682
|
+
const path = join3(dir, ".volty", "targets");
|
|
683
|
+
if (!existsSync4(path))
|
|
684
|
+
return null;
|
|
685
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
686
|
+
}
|
|
687
|
+
function writeTargets(dir, targets) {
|
|
688
|
+
writeFileSync4(join3(dir, ".volty", "targets"), `${JSON.stringify(targets, null, 2)}
|
|
689
|
+
`);
|
|
690
|
+
}
|
|
691
|
+
async function pull(ctx, opts) {
|
|
692
|
+
const { dir, workspaceId, target } = opts;
|
|
693
|
+
const targets = readTargets(dir);
|
|
694
|
+
if (targets) {
|
|
695
|
+
const other = Object.entries(targets).find(([, t]) => t.workspace !== workspaceId);
|
|
696
|
+
if (other) {
|
|
697
|
+
throw new Error(`${dir} holds the project of workspace ${other[1].workspace}: pull ${workspaceId} into its own folder.`);
|
|
698
|
+
}
|
|
699
|
+
} else if (existsSync4(dir) && readdirSync(dir).some((name) => !name.startsWith("."))) {
|
|
700
|
+
throw new Error(`${dir} is not empty and no volty pull wrote it: pull into an empty folder.`);
|
|
701
|
+
}
|
|
702
|
+
const base = targets?.[target]?.version;
|
|
703
|
+
const {
|
|
704
|
+
baseFiles,
|
|
705
|
+
files: tree,
|
|
706
|
+
observed,
|
|
707
|
+
...recorded
|
|
708
|
+
} = await read(ctx, "project pull", { workspace: workspaceId, ...base ? { base } : {} });
|
|
709
|
+
const files = new Map(Object.entries(tree));
|
|
710
|
+
const written = [];
|
|
711
|
+
const deleted = [];
|
|
712
|
+
const kept = [];
|
|
713
|
+
const edited = (rel, local) => base !== undefined && sha256(local) !== baseFiles[rel];
|
|
714
|
+
for (const [rel, text] of [...files].sort(([a], [b]) => a.localeCompare(b))) {
|
|
715
|
+
const path = join3(dir, rel);
|
|
716
|
+
const local = existsSync4(path) ? readFileSync4(path, "utf8") : null;
|
|
717
|
+
if (local === text)
|
|
718
|
+
continue;
|
|
719
|
+
if (local !== null && edited(rel, local)) {
|
|
720
|
+
kept.push({ path: rel, workspace: sha256(text) === baseFiles[rel] ? "unchanged" : "changed" });
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
724
|
+
writeFileSync4(path, text);
|
|
725
|
+
written.push(rel);
|
|
726
|
+
}
|
|
727
|
+
for (const rel of Object.keys(baseFiles).sort()) {
|
|
728
|
+
const path = join3(dir, rel);
|
|
729
|
+
if (files.has(rel) || !existsSync4(path))
|
|
730
|
+
continue;
|
|
731
|
+
if (edited(rel, readFileSync4(path, "utf8"))) {
|
|
732
|
+
kept.push({ path: rel, workspace: "removed" });
|
|
733
|
+
} else {
|
|
734
|
+
unlinkSync(path);
|
|
735
|
+
deleted.push(rel);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
mkdirSync3(join3(dir, ".volty"), { recursive: true });
|
|
739
|
+
writeTargets(dir, { ...targets, [target]: { workspace: recorded.workspaceId, version: recorded.versionId } });
|
|
740
|
+
const observedPath = join3(dir, ".volty", "observed");
|
|
741
|
+
if (existsSync4(observedPath))
|
|
742
|
+
chmodSync(observedPath, 420);
|
|
743
|
+
writeFileSync4(observedPath, observed);
|
|
744
|
+
chmodSync(observedPath, 292);
|
|
745
|
+
return { ...recorded, written, deleted, kept };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/init.ts
|
|
749
|
+
function scaffold(dir) {
|
|
750
|
+
const texts = scaffoldFiles();
|
|
751
|
+
return SCAFFOLD_PATHS.map((path) => {
|
|
752
|
+
const target = join4(dir, path);
|
|
753
|
+
if (existsSync5(target))
|
|
754
|
+
return { path, written: false };
|
|
755
|
+
writeFileSync5(target, texts[path]);
|
|
756
|
+
return { path, written: true };
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
async function init(ctx, opts) {
|
|
760
|
+
const pulled = readTargets(opts.dir) ? null : await pull(ctx, opts);
|
|
761
|
+
return { pulled, files: scaffold(opts.dir) };
|
|
762
|
+
}
|
|
763
|
+
function initLines(dir, files) {
|
|
764
|
+
return [
|
|
765
|
+
`${dir} is set up for an editor:`,
|
|
766
|
+
...files.map((f) => f.written ? ` wrote ${f.path}` : ` kept ${f.path}, which the folder already has`),
|
|
767
|
+
dir === "." ? "Next: run npm install, then open this folder in your editor." : `Next: cd ${dir}, run npm install, then open the folder in your editor.`
|
|
768
|
+
];
|
|
769
|
+
}
|
|
770
|
+
|
|
575
771
|
// src/keys.ts
|
|
576
772
|
var VALUE_FLAGS = new Set(["name", "for", "for-user-id", "expires", "expires-in-days", "families", "cap"]);
|
|
577
773
|
function keysArgv(argv) {
|
|
@@ -648,422 +844,241 @@ function decideProposal(ctx, proposalId, decision, reason) {
|
|
|
648
844
|
});
|
|
649
845
|
}
|
|
650
846
|
|
|
651
|
-
// src/
|
|
652
|
-
import {
|
|
653
|
-
import {
|
|
654
|
-
import { dirname as dirname2, join as join3 } from "node:path";
|
|
655
|
-
async function call(ctx, name, input = {}) {
|
|
656
|
-
const res = await runOp(ctx, { name }, input);
|
|
657
|
-
if (res.status === 422 && res.body?.error?.code === "refused") {
|
|
658
|
-
return { refused: res.body.error.message ?? `${name} refused` };
|
|
659
|
-
}
|
|
660
|
-
if (!res.ok)
|
|
661
|
-
throw new Error(`${name}: ${res.body?.error?.message ?? `HTTP ${res.status}`}`);
|
|
662
|
-
return { value: res.body.structuredContent };
|
|
663
|
-
}
|
|
664
|
-
async function read(ctx, name, input = {}) {
|
|
665
|
-
const out = await call(ctx, name, input);
|
|
666
|
-
if ("refused" in out)
|
|
667
|
-
throw new Error(`${name}: ${out.refused}`);
|
|
668
|
-
return out.value;
|
|
669
|
-
}
|
|
670
|
-
function slugBase(name) {
|
|
671
|
-
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/^-+|-+$/g, "");
|
|
672
|
-
return slug || "source";
|
|
673
|
-
}
|
|
674
|
-
function tsLiteral(value, indent = "") {
|
|
675
|
-
if (value === null || typeof value !== "object")
|
|
676
|
-
return JSON.stringify(value) ?? "undefined";
|
|
677
|
-
const next = `${indent} `;
|
|
678
|
-
if (Array.isArray(value)) {
|
|
679
|
-
if (value.length === 0)
|
|
680
|
-
return "[]";
|
|
681
|
-
return `[
|
|
682
|
-
${value.map((v) => `${next}${tsLiteral(v, next)}`).join(`,
|
|
683
|
-
`)},
|
|
684
|
-
${indent}]`;
|
|
685
|
-
}
|
|
686
|
-
const entries = Object.entries(value).filter(([, v]) => v !== undefined);
|
|
687
|
-
if (entries.length === 0)
|
|
688
|
-
return "{}";
|
|
689
|
-
const key = (k) => /^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k);
|
|
690
|
-
return `{
|
|
691
|
-
${entries.map(([k, v]) => `${next}${key(k)}: ${tsLiteral(v, next)}`).join(`,
|
|
692
|
-
`)},
|
|
693
|
-
${indent}}`;
|
|
694
|
-
}
|
|
695
|
-
function tsFile(header, value) {
|
|
696
|
-
return `// ${header[0]}
|
|
697
|
-
// ${header[1]}
|
|
698
|
-
export default ${tsLiteral(value)}
|
|
699
|
-
`;
|
|
700
|
-
}
|
|
701
|
-
var sha256 = (text) => createHash2("sha256").update(text).digest("hex");
|
|
847
|
+
// src/push.ts
|
|
848
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
|
|
849
|
+
import { join as join5 } from "node:path";
|
|
702
850
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
let lines = this.sections.get(heading);
|
|
707
|
-
if (!lines) {
|
|
708
|
-
lines = [];
|
|
709
|
-
this.sections.set(heading, lines);
|
|
710
|
-
}
|
|
711
|
-
lines.push(line);
|
|
712
|
-
}
|
|
713
|
-
render() {
|
|
714
|
-
const blocks = [...this.sections].map(([heading, lines]) => `${heading}
|
|
715
|
-
${lines.map((l) => `- ${l}`).join(`
|
|
716
|
-
`)}`);
|
|
717
|
-
return `${[OBSERVED_HEAD, ...blocks].join(`
|
|
851
|
+
// ../shared/projects/limits.ts
|
|
852
|
+
var PROJECT_FILE_MAX_CHARS = 2000000;
|
|
853
|
+
var PROJECT_FILES_MAX = 2000;
|
|
718
854
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
855
|
+
// ../shared/text/plural.ts
|
|
856
|
+
function plural(n, noun) {
|
|
857
|
+
return n === 1 ? noun : `${noun}s`;
|
|
722
858
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
859
|
+
|
|
860
|
+
// src/push.ts
|
|
861
|
+
function readProject(dir) {
|
|
862
|
+
const targets = readTargets(dir);
|
|
863
|
+
if (!targets) {
|
|
864
|
+
throw new Error(`${dir} has no .volty/targets, so no volty pull wrote it: pull the workspace into it first.`);
|
|
865
|
+
}
|
|
866
|
+
const files = {};
|
|
867
|
+
let count = 0;
|
|
868
|
+
const walk = (rel) => {
|
|
869
|
+
for (const entry of readdirSync2(join5(dir, rel), { withFileTypes: true })) {
|
|
870
|
+
if (!pushes(entry.name, entry.isDirectory()))
|
|
871
|
+
continue;
|
|
872
|
+
const path = rel ? `${rel}/${entry.name}` : entry.name;
|
|
873
|
+
if (entry.isDirectory()) {
|
|
874
|
+
walk(path);
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
if (!entry.isFile())
|
|
878
|
+
continue;
|
|
879
|
+
if (++count > PROJECT_FILES_MAX) {
|
|
880
|
+
throw new Error(`${dir} holds more than ${PROJECT_FILES_MAX.toLocaleString("en-US")} files, the most a push takes.`);
|
|
881
|
+
}
|
|
882
|
+
const text = readFileSync5(join5(dir, path), "utf8");
|
|
883
|
+
if (text.length > PROJECT_FILE_MAX_CHARS) {
|
|
884
|
+
throw new Error(`${path} holds ${text.length.toLocaleString("en-US")} characters, over the ${PROJECT_FILE_MAX_CHARS.toLocaleString("en-US")} a push takes per file.`);
|
|
885
|
+
}
|
|
886
|
+
files[path] = text;
|
|
734
887
|
}
|
|
735
888
|
};
|
|
736
|
-
|
|
889
|
+
walk("");
|
|
890
|
+
return { targets, files };
|
|
891
|
+
}
|
|
892
|
+
function pickTargets(targets, env) {
|
|
893
|
+
const names = Object.keys(targets);
|
|
894
|
+
if (env === "all")
|
|
895
|
+
return names;
|
|
896
|
+
if (env !== undefined) {
|
|
897
|
+
if (!targets[env])
|
|
898
|
+
throw new Error(`.volty/targets has no target ${env}: it has ${names.join(", ") || "none"}.`);
|
|
899
|
+
return [env];
|
|
900
|
+
}
|
|
901
|
+
if (names.length === 1)
|
|
902
|
+
return names;
|
|
903
|
+
throw new Error(`.volty/targets has ${names.length} targets: name one with --env (${names.join(", ")}) or --env all.`);
|
|
904
|
+
}
|
|
905
|
+
function renderPlan(plan) {
|
|
906
|
+
const out = [];
|
|
907
|
+
for (const item of plan.items) {
|
|
908
|
+
if (item.change === "none" && item.lines.length === 0 && item.findings.length === 0)
|
|
909
|
+
continue;
|
|
910
|
+
const [first, ...more] = item.lines;
|
|
911
|
+
const head = item.change !== "none" ? item.change : first ?? "no change";
|
|
912
|
+
out.push(`${item.name}: ${head}`);
|
|
913
|
+
for (const line of item.change !== "none" ? item.lines : more)
|
|
914
|
+
out.push(` ${line}`);
|
|
915
|
+
for (const line of item.liveSince)
|
|
916
|
+
out.push(` live since your pull: ${line}`);
|
|
917
|
+
for (const f of item.findings)
|
|
918
|
+
out.push(` ${f.severity}: ${f.message}`);
|
|
919
|
+
}
|
|
920
|
+
out.push(plan.summary);
|
|
737
921
|
return out;
|
|
738
922
|
}
|
|
739
|
-
function
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
...action,
|
|
746
|
-
...teamId ? { team: rule.teamSlug } : {},
|
|
747
|
-
...userId ? { member: rule.memberEmails[0] ?? null } : {},
|
|
748
|
-
...userIds ? { members: rule.memberEmails } : {}
|
|
923
|
+
function refusalOf(plan, confirm) {
|
|
924
|
+
if (!plan.runnable) {
|
|
925
|
+
const why = [];
|
|
926
|
+
if (plan.stale) {
|
|
927
|
+
const stale = plan.items.filter((i) => i.change !== "none" && i.liveSince.length > 0).map((i) => i.name);
|
|
928
|
+
why.push(`${stale.length > 0 ? stale.join(", ") : "The workspace"} changed live since your pull: pull again, then push.`);
|
|
749
929
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const { inboxes } = await read(ctx, "inboxes list");
|
|
755
|
-
const rows = inboxes.filter((i) => i.type === "shared").sort((a, b) => a.id.localeCompare(b.id));
|
|
756
|
-
const got = await pooled(rows, async (row) => {
|
|
757
|
-
if (row.role === null)
|
|
758
|
-
return null;
|
|
759
|
-
const inbox = await read(ctx, "inboxes get", { inboxId: row.id });
|
|
760
|
-
const flow = inbox.webChat ? await read(ctx, "chat-flow get", { inboxId: row.id, part: "source" }) : null;
|
|
761
|
-
return { inbox, flow };
|
|
762
|
-
});
|
|
763
|
-
rows.forEach((row, i) => {
|
|
764
|
-
const one = got[i];
|
|
765
|
-
if (!one) {
|
|
766
|
-
out.observed.add("Inboxes not pulled", `${row.name}: you are not on this inbox, so its settings are not readable`);
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
const { inbox, flow } = one;
|
|
770
|
-
out.slugById.set(row.id, inbox.slug);
|
|
771
|
-
const dir = `inboxes/${inbox.slug}`;
|
|
772
|
-
if (flow?.published)
|
|
773
|
-
out.files.set(`${dir}/quick-paths.ts`, flow.published.source);
|
|
774
|
-
if (flow && flow.revision > 0 && flow.source !== flow.published?.source) {
|
|
775
|
-
out.observed.add("Quick paths with unpublished changes (the file holds the published version)", `${dir}/quick-paths.ts: the draft at revision ${flow.revision} is not published`);
|
|
930
|
+
for (const item of plan.items) {
|
|
931
|
+
for (const f of item.findings)
|
|
932
|
+
if (f.severity === "error")
|
|
933
|
+
why.push(`${item.name}: ${f.message}`);
|
|
776
934
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
`The ${inbox.name} inbox: hours, reply targets, hand-off, priority guide, routing, Volty's reply mode and its web chat.`,
|
|
782
|
-
"Written by volty pull from the live inbox. Who works the inbox, team members and channels stay in the app."
|
|
783
|
-
], {
|
|
784
|
-
name: inbox.name,
|
|
785
|
-
icon: inbox.icon,
|
|
786
|
-
autoEnrollOrgMembers: inbox.autoEnrollOrgMembers,
|
|
787
|
-
businessHours: inbox.businessHours,
|
|
788
|
-
idleClose: inbox.idleClose,
|
|
789
|
-
firstResponseMinutes: inbox.firstResponseMinutes,
|
|
790
|
-
handoff: inbox.handoff,
|
|
791
|
-
priority: inbox.priority,
|
|
792
|
-
routing: { ...routing, rules: routing.rules.map(ruleOut) },
|
|
793
|
-
volty: volty ? { agent: agentName, ...seat } : null,
|
|
794
|
-
webChat: webChat ? {
|
|
795
|
-
...chat,
|
|
796
|
-
handoffWhatsappNumber: webChat.handoffAddresses.whatsapp,
|
|
797
|
-
handoffEmailAddress: webChat.handoffAddresses.email
|
|
798
|
-
} : null,
|
|
799
|
-
quickPaths: flow ? { enabled: flow.enabled } : undefined
|
|
800
|
-
}));
|
|
801
|
-
});
|
|
802
|
-
return out;
|
|
803
|
-
}
|
|
804
|
-
async function pullInstructions(ctx, inboxes) {
|
|
805
|
-
const out = part();
|
|
806
|
-
const [got, { slugById }] = await Promise.all([read(ctx, "instructions get"), inboxes]);
|
|
807
|
-
if (got.org.text)
|
|
808
|
-
out.files.set("instructions/org.md", got.org.text);
|
|
809
|
-
if (got.team.text)
|
|
810
|
-
out.files.set("instructions/team.md", got.team.text);
|
|
811
|
-
for (const row of got.inboxes) {
|
|
812
|
-
const slug = slugById.get(row.inboxId);
|
|
813
|
-
if (slug && row.text)
|
|
814
|
-
out.files.set(`inboxes/${slug}/instructions.md`, row.text);
|
|
935
|
+
if (why.length === 0)
|
|
936
|
+
why.push(plan.summary);
|
|
937
|
+
return `Nothing was pushed. ${why.join(`
|
|
938
|
+
`)}`;
|
|
815
939
|
}
|
|
816
|
-
|
|
940
|
+
const destructive = plan.items.filter((i) => i.destructive && i.change !== "none").map((i) => i.name);
|
|
941
|
+
if (destructive.length > 0 && !confirm) {
|
|
942
|
+
return `Nothing was pushed: ${destructive.join(", ")} ${destructive.length === 1 ? "is" : "are"} destructive. Read the plan, then push again with --confirm.`;
|
|
943
|
+
}
|
|
944
|
+
return null;
|
|
817
945
|
}
|
|
818
|
-
async function
|
|
819
|
-
const {
|
|
820
|
-
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
return;
|
|
946
|
+
async function applyItem(ctx, item) {
|
|
947
|
+
const base = { path: item.path, name: item.name, lines: item.lines };
|
|
948
|
+
let pending = false;
|
|
949
|
+
for (const step of item.steps) {
|
|
950
|
+
let res;
|
|
951
|
+
try {
|
|
952
|
+
res = await runOp(ctx, { name: step.op }, step.input);
|
|
953
|
+
} catch (err) {
|
|
954
|
+
const detail = `${step.op}: ${err instanceof Error ? err.message : String(err)}`;
|
|
955
|
+
return { result: { ...base, result: "failed", detail }, stop: true };
|
|
829
956
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
slug = `${base}-${n}`;
|
|
834
|
-
taken.add(slug);
|
|
835
|
-
const { id: _id, scope, automation, ...rest } = one.value.file;
|
|
836
|
-
out.files.set(`sources/${slug}.ts`, tsFile([
|
|
837
|
-
`The ${row.name} source: its settings, the names of its secrets (never a value), lanes, scope and tool modes.`,
|
|
838
|
-
"Written by volty pull. Secret values, sign-ins and which tools an admin approved stay in the app."
|
|
839
|
-
], {
|
|
840
|
-
...rest,
|
|
841
|
-
scope: { mode: scope.mode, inboxes: scope.inboxes.map((i) => i.slug), privateInboxes: scope.privateInboxes },
|
|
842
|
-
automation: automation?.slug ?? null
|
|
843
|
-
}));
|
|
844
|
-
});
|
|
845
|
-
return out;
|
|
846
|
-
}
|
|
847
|
-
var notDeployed = (row) => `${row.slug}: ${row.pendingApproval ? "its first version waits for approval" : "no version is running"}`;
|
|
848
|
-
async function pullAutomations(ctx) {
|
|
849
|
-
const out = part();
|
|
850
|
-
const { automations } = await read(ctx, "automations list");
|
|
851
|
-
const got = await pooled(automations, async (row) => {
|
|
852
|
-
if (!row.deployed)
|
|
853
|
-
return null;
|
|
854
|
-
const first = await read(ctx, "automations get", { id: row.id });
|
|
855
|
-
if (first.sourceLabel !== "pending" || !first.active?.version)
|
|
856
|
-
return { got: first, waiting: false };
|
|
857
|
-
return {
|
|
858
|
-
got: await read(ctx, "automations get", { id: row.id, version: first.active.version }),
|
|
859
|
-
waiting: true
|
|
860
|
-
};
|
|
861
|
-
});
|
|
862
|
-
automations.forEach((row, i) => {
|
|
863
|
-
const one = got[i];
|
|
864
|
-
if (!one) {
|
|
865
|
-
out.observed.add("Automations not deployed", notDeployed(row));
|
|
866
|
-
return;
|
|
957
|
+
if (res.status === 202) {
|
|
958
|
+
const changeId = res.body.structuredContent?.proposalId;
|
|
959
|
+
return { result: { ...base, result: "held", ...changeId ? { changeId } : {} }, stop: false };
|
|
867
960
|
}
|
|
868
|
-
if (
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
out.files.set(`automations/${row.slug}.ts`, one.got.source);
|
|
872
|
-
});
|
|
873
|
-
return out;
|
|
874
|
-
}
|
|
875
|
-
async function pullPages(ctx) {
|
|
876
|
-
const out = part();
|
|
877
|
-
const { miniApps } = await read(ctx, "mini-apps list");
|
|
878
|
-
const got = await pooled(miniApps, async (row) => {
|
|
879
|
-
if (!row.deployed)
|
|
880
|
-
return null;
|
|
881
|
-
const first = await read(ctx, "mini-apps get", { id: row.id });
|
|
882
|
-
if (!first.pending)
|
|
883
|
-
return { got: first, waiting: false };
|
|
884
|
-
const active = first.deploys.find((d) => d.status === "active");
|
|
885
|
-
return {
|
|
886
|
-
got: active?.version ? await read(ctx, "mini-apps get", { id: row.id, version: active.version }) : first,
|
|
887
|
-
waiting: true
|
|
888
|
-
};
|
|
889
|
-
});
|
|
890
|
-
miniApps.forEach((row, i) => {
|
|
891
|
-
const one = got[i];
|
|
892
|
-
if (!one) {
|
|
893
|
-
out.observed.add("Pages not deployed", notDeployed(row));
|
|
894
|
-
return;
|
|
961
|
+
if (res.status === 422) {
|
|
962
|
+
const detail = res.body?.error?.message ?? `${step.op} refused`;
|
|
963
|
+
return { result: { ...base, result: "refused", detail }, stop: false };
|
|
895
964
|
}
|
|
896
|
-
if (
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
out.files.set(`pages/${row.slug}/entry.tsx`, one.got.entrySource);
|
|
900
|
-
if (one.got.bffSource !== null)
|
|
901
|
-
out.files.set(`pages/${row.slug}/bff.ts`, one.got.bffSource);
|
|
902
|
-
});
|
|
903
|
-
return out;
|
|
904
|
-
}
|
|
905
|
-
var DRIVE_FOLDER_CAP = 100;
|
|
906
|
-
async function walkDrive(ctx) {
|
|
907
|
-
const files = [];
|
|
908
|
-
let queue = ["/"];
|
|
909
|
-
let walked = 0;
|
|
910
|
-
while (queue.length > 0 && walked < DRIVE_FOLDER_CAP) {
|
|
911
|
-
const level = queue.slice(0, DRIVE_FOLDER_CAP - walked);
|
|
912
|
-
walked += level.length;
|
|
913
|
-
const listed = await pooled(level, (path) => read(ctx, "drive list-folder", { scope: "organization", path }));
|
|
914
|
-
const children = [];
|
|
915
|
-
for (const { entries } of listed) {
|
|
916
|
-
for (const entry of entries) {
|
|
917
|
-
if (entry.kind === "folder")
|
|
918
|
-
children.push(entry.path);
|
|
919
|
-
else
|
|
920
|
-
files.push(entry.path);
|
|
921
|
-
}
|
|
965
|
+
if (res.status !== 200) {
|
|
966
|
+
const detail = `${step.op}: ${res.body?.error?.message ?? `HTTP ${res.status}`}`;
|
|
967
|
+
return { result: { ...base, result: "failed", detail }, stop: true };
|
|
922
968
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
const out = part();
|
|
929
|
-
const [learned, drive, { tables }] = await Promise.all([
|
|
930
|
-
read(ctx, "learned list"),
|
|
931
|
-
walkDrive(ctx),
|
|
932
|
-
read(ctx, "tables list")
|
|
933
|
-
]);
|
|
934
|
-
for (const rule of learned.rules)
|
|
935
|
-
out.observed.add("Learned rules (Volty writes them; a push never touches them)", rule.text);
|
|
936
|
-
for (const path of drive.files)
|
|
937
|
-
out.observed.add(DRIVE_HEADING, path);
|
|
938
|
-
if (drive.unread > 0)
|
|
939
|
-
out.observed.add(DRIVE_HEADING, `${drive.unread} more folders not listed`);
|
|
940
|
-
for (const table of tables)
|
|
941
|
-
out.observed.add("Tables (their rows stay in the workspace)", table.name);
|
|
942
|
-
return out;
|
|
943
|
-
}
|
|
944
|
-
function agentsMd(workspaceId, sources) {
|
|
945
|
-
const systems = [...sources].sort((a, b) => a.name.localeCompare(b.name)).map((s) => `- ${s.name} (${s.kind})`);
|
|
946
|
-
return `# Volty project
|
|
947
|
-
|
|
948
|
-
This folder is one Volty workspace's setup as files, written by \`volty pull\` from the live workspace \`${workspaceId}\`.
|
|
949
|
-
|
|
950
|
-
- \`instructions/org.md\`: the instructions Volty reads everywhere. \`instructions/team.md\`: how Volty works with staff, read only when it talks to staff.
|
|
951
|
-
- \`inboxes/<slug>/inbox.ts\`: one inbox's settings. Beside it, \`instructions.md\` is that inbox's own instructions and \`quick-paths.ts\` its web chat's quick paths.
|
|
952
|
-
- \`sources/<slug>.ts\`: a connected source's settings, the names of its secrets (never a value), lanes and tool modes.
|
|
953
|
-
- \`automations/<slug>.ts\`: an automation's running source.
|
|
954
|
-
- \`pages/<slug>/\`: a page's \`entry.tsx\` and \`bff.ts\`.
|
|
955
|
-
- \`volty.config.ts\`: the workspace this project deploys to.
|
|
956
|
-
- \`.volty/targets\`: the version this folder was pulled from. \`.volty/observed\`: what the workspace holds outside the project (Learned rules, Drive files, tables, sources with no file yet), read-only.
|
|
957
|
-
|
|
958
|
-
Systems connected to this workspace:
|
|
959
|
-
${systems.length > 0 ? systems.join(`
|
|
960
|
-
`) : "- none"}
|
|
961
|
-
|
|
962
|
-
Pushing the folder back is not built yet. Run \`volty pull\` again to bring the folder up to date, and \`volty docs\` for every operation.
|
|
963
|
-
`;
|
|
964
|
-
}
|
|
965
|
-
function configFile(target, workspaceId) {
|
|
966
|
-
return tsFile([
|
|
967
|
-
"Where this project deploys: each target names one workspace by id.",
|
|
968
|
-
"Written by volty pull; the version each target was pulled from is in .volty/targets."
|
|
969
|
-
], { targets: { [target]: { workspace: workspaceId } } });
|
|
970
|
-
}
|
|
971
|
-
function readTargets(dir) {
|
|
972
|
-
const path = join3(dir, ".volty", "targets");
|
|
973
|
-
if (!existsSync4(path))
|
|
974
|
-
return null;
|
|
975
|
-
return JSON.parse(readFileSync4(path, "utf8"));
|
|
976
|
-
}
|
|
977
|
-
async function pull(ctx, opts) {
|
|
978
|
-
const { dir, workspaceId, target } = opts;
|
|
979
|
-
const targets = readTargets(dir);
|
|
980
|
-
if (targets) {
|
|
981
|
-
const other = Object.entries(targets).find(([, t]) => t.workspace !== workspaceId);
|
|
982
|
-
if (other) {
|
|
983
|
-
throw new Error(`${dir} holds the project of workspace ${other[1].workspace}: pull ${workspaceId} into its own folder.`);
|
|
969
|
+
const answer = res.body.structuredContent;
|
|
970
|
+
if (answer?.deployStatus === "changes_requested") {
|
|
971
|
+
const detail = res.body.content?.map((c) => c.text ?? "").join(`
|
|
972
|
+
`) || `${step.op} was sent back by the reviewer`;
|
|
973
|
+
return { result: { ...base, result: "refused", detail }, stop: false };
|
|
984
974
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const
|
|
1009
|
-
const {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (
|
|
975
|
+
if (answer?.deployStatus === "pending_approval")
|
|
976
|
+
pending = true;
|
|
977
|
+
}
|
|
978
|
+
return { result: { ...base, result: pending ? "pending" : "applied" }, stop: false };
|
|
979
|
+
}
|
|
980
|
+
var STORED = "stored with the version for the next pull";
|
|
981
|
+
function resultLine(r) {
|
|
982
|
+
switch (r.result) {
|
|
983
|
+
case "applied":
|
|
984
|
+
return r.detail ? `${r.name}: applied, ${r.detail}` : `${r.name}: applied`;
|
|
985
|
+
case "pending":
|
|
986
|
+
return `${r.name}: pending approval, the running version stays live until an admin approves`;
|
|
987
|
+
case "held":
|
|
988
|
+
return `${r.name}: held as change ${r.changeId ?? "(no id)"} for an admin. The rest of this item did not run and will not run on approval, so push again once it is decided (volty changes lists it)`;
|
|
989
|
+
case "refused":
|
|
990
|
+
return `${r.name}: refused: ${r.detail}`;
|
|
991
|
+
case "failed":
|
|
992
|
+
return `${r.name}: failed: ${r.detail}`;
|
|
993
|
+
case "skipped":
|
|
994
|
+
return r.detail ? `${r.name}: not applied, ${r.detail}` : `${r.name}: skipped, after the failure above`;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
async function pushTarget(ctx, opts) {
|
|
998
|
+
const { dir, target, files, targets } = opts;
|
|
999
|
+
const { workspace, version } = targets[target];
|
|
1000
|
+
const base = version ? { base: version } : {};
|
|
1001
|
+
const plan = await read(ctx, "project push", { workspace, target, files, ...base });
|
|
1002
|
+
const lines = renderPlan(plan);
|
|
1003
|
+
const refusal = refusalOf(plan, opts.confirm || opts.dryRun);
|
|
1004
|
+
if (opts.dryRun)
|
|
1005
|
+
return { exitCode: plan.runnable ? 0 : 1, lines, json: plan, ...refusal ? { error: refusal } : {} };
|
|
1006
|
+
if (refusal)
|
|
1007
|
+
return { exitCode: 1, lines, json: plan, error: refusal };
|
|
1008
|
+
const results = [];
|
|
1009
|
+
let stopped = false;
|
|
1010
|
+
for (const item of plan.items) {
|
|
1011
|
+
if (item.change === "none")
|
|
1022
1012
|
continue;
|
|
1023
|
-
if (
|
|
1024
|
-
|
|
1013
|
+
if (stopped) {
|
|
1014
|
+
results.push({ path: item.path, name: item.name, lines: item.lines, result: "skipped" });
|
|
1025
1015
|
continue;
|
|
1026
1016
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
written.push(rel);
|
|
1030
|
-
}
|
|
1031
|
-
for (const rel of Object.keys(baseFiles).sort()) {
|
|
1032
|
-
const path = join3(dir, rel);
|
|
1033
|
-
if (files.has(rel) || !existsSync4(path))
|
|
1017
|
+
if (item.steps.length === 0 && item.stored) {
|
|
1018
|
+
results.push({ path: item.path, name: item.name, lines: item.lines, result: "applied", detail: STORED });
|
|
1034
1019
|
continue;
|
|
1035
|
-
if (edited(rel, readFileSync4(path, "utf8"))) {
|
|
1036
|
-
kept.push({ path: rel, workspace: "removed" });
|
|
1037
|
-
} else {
|
|
1038
|
-
unlinkSync(path);
|
|
1039
|
-
deleted.push(rel);
|
|
1040
1020
|
}
|
|
1021
|
+
if (item.steps.length === 0) {
|
|
1022
|
+
const detail = item.findings.find((f) => f.severity === "warn")?.message ?? "nothing to run for this kind yet";
|
|
1023
|
+
results.push({ path: item.path, name: item.name, lines: item.lines, result: "skipped", detail });
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const { result, stop } = await applyItem(ctx, item);
|
|
1027
|
+
results.push(result);
|
|
1028
|
+
stopped = stop;
|
|
1029
|
+
}
|
|
1030
|
+
if (results.length === 0)
|
|
1031
|
+
return { exitCode: 0, lines, json: plan };
|
|
1032
|
+
const resultsOut = results.map((r) => resultLine(r));
|
|
1033
|
+
if (results.every((r) => r.result === "skipped")) {
|
|
1034
|
+
const n = results.length;
|
|
1035
|
+
return {
|
|
1036
|
+
exitCode: 1,
|
|
1037
|
+
lines: [...lines, ...resultsOut],
|
|
1038
|
+
json: { items: results },
|
|
1039
|
+
error: `Nothing was pushed: ${n} ${plural(n, "item")} ${n === 1 ? "has" : "have"} nothing to run yet.`
|
|
1040
|
+
};
|
|
1041
1041
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1042
|
+
let record;
|
|
1043
|
+
try {
|
|
1044
|
+
record = await read(ctx, "project record-push", {
|
|
1045
|
+
workspace,
|
|
1046
|
+
target,
|
|
1047
|
+
files,
|
|
1048
|
+
...base,
|
|
1049
|
+
items: results
|
|
1050
|
+
});
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1053
|
+
return {
|
|
1054
|
+
exitCode: 1,
|
|
1055
|
+
lines: [...lines, ...resultsOut],
|
|
1056
|
+
json: { items: results },
|
|
1057
|
+
error: `The items above ran, but recording the push failed (${message}). .volty/targets keeps the old base: push again.`
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
targets[target] = { workspace, version: record.versionId };
|
|
1061
|
+
writeTargets(dir, targets);
|
|
1062
|
+
const count = (o) => results.filter((r) => r.result === o).length;
|
|
1063
|
+
const skipped = count("skipped");
|
|
1064
|
+
const summary = `Pushed ${results.length} ${plural(results.length, "item")} to ${target}: ${count("applied")} applied, ${count("held")} held, ${count("pending")} pending approval, ${count("refused")} refused, ${count("failed")} failed${skipped > 0 ? `, ${skipped} skipped` : ""}.`;
|
|
1065
|
+
return {
|
|
1066
|
+
exitCode: record.status === "partial" ? 1 : 0,
|
|
1067
|
+
lines: [...lines, ...resultsOut, summary],
|
|
1068
|
+
json: { ...record, items: results }
|
|
1046
1069
|
};
|
|
1047
|
-
writeFileSync4(join3(dir, ".volty", "targets"), `${JSON.stringify(nextTargets, null, 2)}
|
|
1048
|
-
`);
|
|
1049
|
-
const observedPath = join3(dir, ".volty", "observed");
|
|
1050
|
-
if (existsSync4(observedPath))
|
|
1051
|
-
chmodSync(observedPath, 420);
|
|
1052
|
-
writeFileSync4(observedPath, observed.render());
|
|
1053
|
-
chmodSync(observedPath, 292);
|
|
1054
|
-
return { ...recorded, written, deleted, kept };
|
|
1055
1070
|
}
|
|
1056
1071
|
|
|
1057
1072
|
// src/update.ts
|
|
1058
|
-
import { mkdirSync as mkdirSync4, readFileSync as
|
|
1073
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1059
1074
|
import { homedir as homedir2 } from "node:os";
|
|
1060
|
-
import { dirname as dirname3, join as
|
|
1061
|
-
var CACHE_PATH =
|
|
1075
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
1076
|
+
var CACHE_PATH = join6(homedir2(), ".config", "volty", "update-check.json");
|
|
1062
1077
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
1063
1078
|
var REGISTRY_LATEST = "https://registry.npmjs.org/@voltade/volty/latest";
|
|
1064
1079
|
function currentVersion() {
|
|
1065
1080
|
try {
|
|
1066
|
-
const pkg = JSON.parse(
|
|
1081
|
+
const pkg = JSON.parse(readFileSync6(join6(import.meta.dirname, "..", "package.json"), "utf8"));
|
|
1067
1082
|
return pkg.version ?? null;
|
|
1068
1083
|
} catch {
|
|
1069
1084
|
return null;
|
|
@@ -1081,7 +1096,7 @@ function cmpVersion(a, b) {
|
|
|
1081
1096
|
}
|
|
1082
1097
|
function readCache() {
|
|
1083
1098
|
try {
|
|
1084
|
-
return JSON.parse(
|
|
1099
|
+
return JSON.parse(readFileSync6(CACHE_PATH, "utf8"));
|
|
1085
1100
|
} catch {
|
|
1086
1101
|
return null;
|
|
1087
1102
|
}
|
|
@@ -1089,7 +1104,7 @@ function readCache() {
|
|
|
1089
1104
|
function writeCache(cache) {
|
|
1090
1105
|
try {
|
|
1091
1106
|
mkdirSync4(dirname3(CACHE_PATH), { recursive: true });
|
|
1092
|
-
|
|
1107
|
+
writeFileSync6(CACHE_PATH, `${JSON.stringify(cache)}
|
|
1093
1108
|
`);
|
|
1094
1109
|
} catch {}
|
|
1095
1110
|
}
|
|
@@ -1316,10 +1331,13 @@ var KEPT_NOTE = {
|
|
|
1316
1331
|
changed: ", and the workspace changed it too: delete it and pull again for the workspace copy",
|
|
1317
1332
|
removed: ", and the workspace no longer has it"
|
|
1318
1333
|
};
|
|
1319
|
-
|
|
1334
|
+
function folderArg(rest, command) {
|
|
1320
1335
|
const [dir = ".", ...extra] = rest;
|
|
1321
1336
|
if (extra.length > 0 || dir.startsWith("--"))
|
|
1322
|
-
fail(
|
|
1337
|
+
fail(`usage: volty ${command} [dir] [--workspace <id>]`);
|
|
1338
|
+
return dir;
|
|
1339
|
+
}
|
|
1340
|
+
async function workspaceOf(ctx) {
|
|
1323
1341
|
let workspaceId = ctx.organizationId;
|
|
1324
1342
|
if (!workspaceId) {
|
|
1325
1343
|
const res = await whoami(ctx);
|
|
@@ -1329,23 +1347,94 @@ async function handlePull(rest, ctx, env) {
|
|
|
1329
1347
|
}
|
|
1330
1348
|
if (!workspaceId)
|
|
1331
1349
|
fail("this key names no workspace: run `volty auth login`");
|
|
1332
|
-
|
|
1350
|
+
return workspaceId;
|
|
1351
|
+
}
|
|
1352
|
+
function pullLines(dir, out) {
|
|
1333
1353
|
if (out.unchanged && out.written.length + out.deleted.length + out.kept.length === 0) {
|
|
1334
|
-
|
|
1335
|
-
`);
|
|
1336
|
-
return;
|
|
1354
|
+
return [`${dir} is up to date with version ${out.versionId} of workspace ${out.workspaceId}.`];
|
|
1337
1355
|
}
|
|
1338
|
-
|
|
1356
|
+
return [
|
|
1339
1357
|
`Pulled workspace ${out.workspaceId} into ${dir}: version ${out.versionId}${out.unchanged ? " (unchanged)" : ""}.`,
|
|
1340
1358
|
...out.written.map((f) => ` wrote ${f}`),
|
|
1341
1359
|
...out.deleted.map((f) => ` deleted ${f}, which the workspace no longer has`),
|
|
1342
1360
|
...out.kept.map((f) => ` kept ${f.path}: edited here${KEPT_NOTE[f.workspace]}`),
|
|
1343
1361
|
"What the project does not hold is listed in .volty/observed."
|
|
1344
1362
|
];
|
|
1363
|
+
}
|
|
1364
|
+
async function handlePull(rest, ctx, env) {
|
|
1365
|
+
const dir = folderArg(rest, "pull");
|
|
1366
|
+
const out = await pull(ctx, { dir, workspaceId: await workspaceOf(ctx), target: env });
|
|
1367
|
+
process.stdout.write(`${pullLines(dir, out).join(`
|
|
1368
|
+
`)}
|
|
1369
|
+
`);
|
|
1370
|
+
}
|
|
1371
|
+
async function handleInit(rest, ctx, env) {
|
|
1372
|
+
const dir = folderArg(rest, "init");
|
|
1373
|
+
const out = await init(ctx, { dir, workspaceId: await workspaceOf(ctx), target: env });
|
|
1374
|
+
const lines = [...out.pulled ? pullLines(dir, out.pulled) : [], ...initLines(dir, out.files)];
|
|
1345
1375
|
process.stdout.write(`${lines.join(`
|
|
1346
1376
|
`)}
|
|
1347
1377
|
`);
|
|
1348
1378
|
}
|
|
1379
|
+
var PUSH_USAGE = "usage: volty push [dir] [--env <target>|all] [--dry-run] [--confirm] [--json]";
|
|
1380
|
+
async function handlePush(argv, config, envFlag) {
|
|
1381
|
+
const dryRun = takeBool(argv, "--dry-run");
|
|
1382
|
+
const confirm = takeBool(dryRun.rest, "--confirm");
|
|
1383
|
+
const json = takeBool(confirm.rest, "--json");
|
|
1384
|
+
const [dir = ".", ...extra] = json.rest;
|
|
1385
|
+
if (extra.length > 0 || dir.startsWith("--"))
|
|
1386
|
+
fail(PUSH_USAGE);
|
|
1387
|
+
const project = readProject(dir);
|
|
1388
|
+
const names = pickTargets(project.targets, envFlag);
|
|
1389
|
+
const answers = {};
|
|
1390
|
+
let exitCode = 0;
|
|
1391
|
+
for (const name of names) {
|
|
1392
|
+
const login = config.contexts[name];
|
|
1393
|
+
if (!login?.apiKey)
|
|
1394
|
+
fail(`not logged in to '${name}', the env target ${name} runs on: run \`volty auth login --env ${name}\``);
|
|
1395
|
+
const { workspace } = project.targets[name];
|
|
1396
|
+
const selected = await ensureOrgKey(login, workspace, (line) => process.stderr.write(`${line}
|
|
1397
|
+
`));
|
|
1398
|
+
if (selected.minted)
|
|
1399
|
+
saveConfig(config);
|
|
1400
|
+
const ctx = { ...login, apiKey: selected.apiKey, organizationId: selected.organizationId };
|
|
1401
|
+
setOrgOverride(selected.organizationId);
|
|
1402
|
+
if (isProdShaped(name, login.baseUrl)) {
|
|
1403
|
+
const res = await whoami(ctx);
|
|
1404
|
+
if ("error" in res)
|
|
1405
|
+
fail(res.error);
|
|
1406
|
+
process.stderr.write(`→ ${name} · ${res.who.organizationName} (${res.who.organizationId}) · ${res.who.role}
|
|
1407
|
+
`);
|
|
1408
|
+
}
|
|
1409
|
+
if (names.length > 1 && !json.present)
|
|
1410
|
+
process.stdout.write(`Target ${name}, workspace ${workspace}:
|
|
1411
|
+
`);
|
|
1412
|
+
const out = await pushTarget(ctx, {
|
|
1413
|
+
dir,
|
|
1414
|
+
target: name,
|
|
1415
|
+
...project,
|
|
1416
|
+
dryRun: dryRun.present,
|
|
1417
|
+
confirm: confirm.present
|
|
1418
|
+
});
|
|
1419
|
+
answers[name] = out.json;
|
|
1420
|
+
if (!json.present)
|
|
1421
|
+
process.stdout.write(`${out.lines.join(`
|
|
1422
|
+
`)}
|
|
1423
|
+
`);
|
|
1424
|
+
if (out.error)
|
|
1425
|
+
process.stderr.write(`volty: ${out.error}
|
|
1426
|
+
`);
|
|
1427
|
+
exitCode = out.exitCode;
|
|
1428
|
+
if (exitCode !== 0)
|
|
1429
|
+
break;
|
|
1430
|
+
}
|
|
1431
|
+
if (json.present) {
|
|
1432
|
+
const body = names.length === 1 ? answers[names[0]] : answers;
|
|
1433
|
+
process.stdout.write(`${JSON.stringify(body, null, 2)}
|
|
1434
|
+
`);
|
|
1435
|
+
}
|
|
1436
|
+
process.exitCode = exitCode;
|
|
1437
|
+
}
|
|
1349
1438
|
async function handleDocs(config, env) {
|
|
1350
1439
|
const ctx = ensureContext(config, env);
|
|
1351
1440
|
if (!ctx.baseUrl)
|
|
@@ -1359,16 +1448,16 @@ function handleSkill(rest) {
|
|
|
1359
1448
|
if (rest[0] !== "install")
|
|
1360
1449
|
fail("usage: volty skill install");
|
|
1361
1450
|
const candidates = [
|
|
1362
|
-
|
|
1363
|
-
|
|
1451
|
+
join7(import.meta.dirname, "..", "skill", "volty-cli-ops", "SKILL.md"),
|
|
1452
|
+
join7(import.meta.dirname, "..", "..", ".claude", "skills", "volty-cli-ops", "SKILL.md")
|
|
1364
1453
|
];
|
|
1365
|
-
const source = candidates.find((p) =>
|
|
1454
|
+
const source = candidates.find((p) => existsSync6(p));
|
|
1366
1455
|
if (!source)
|
|
1367
1456
|
fail("bundled SKILL.md not found — reinstall the @voltade/volty package");
|
|
1368
|
-
const destDir =
|
|
1457
|
+
const destDir = join7(homedir3(), ".claude", "skills", "volty-cli-ops");
|
|
1369
1458
|
mkdirSync5(destDir, { recursive: true });
|
|
1370
|
-
|
|
1371
|
-
process.stdout.write(`Installed the volty-cli-ops skill → ${
|
|
1459
|
+
writeFileSync7(join7(destDir, "SKILL.md"), readFileSync7(source, "utf8"));
|
|
1460
|
+
process.stdout.write(`Installed the volty-cli-ops skill → ${join7(destDir, "SKILL.md")}
|
|
1372
1461
|
`);
|
|
1373
1462
|
}
|
|
1374
1463
|
async function main() {
|
|
@@ -1379,9 +1468,14 @@ async function main() {
|
|
|
1379
1468
|
argv = orgExtract.rest;
|
|
1380
1469
|
const cacheFlag = takeBool(argv, "--no-cache");
|
|
1381
1470
|
argv = cacheFlag.rest;
|
|
1382
|
-
const workspaceExtract = argv[0] === "pull" ? takeValue(argv, "--workspace") : { value: undefined, rest: argv };
|
|
1471
|
+
const workspaceExtract = argv[0] === "pull" || argv[0] === "init" ? takeValue(argv, "--workspace") : { value: undefined, rest: argv };
|
|
1383
1472
|
argv = workspaceExtract.rest;
|
|
1384
1473
|
const config = loadConfig();
|
|
1474
|
+
if (argv[0] === "push") {
|
|
1475
|
+
if (orgExtract.value)
|
|
1476
|
+
fail("volty push takes its workspace from .volty/targets: leave out --org");
|
|
1477
|
+
return handlePush(argv.slice(1), config, envExtract.value ?? (process.env.VOLTY_ENV || undefined));
|
|
1478
|
+
}
|
|
1385
1479
|
const env = activeEnv(config, envExtract.value ?? (process.env.VOLTY_ENV || undefined));
|
|
1386
1480
|
const peek = config.contexts[env];
|
|
1387
1481
|
const prod = isProdShaped(env, peek?.baseUrl);
|
|
@@ -1431,6 +1525,8 @@ async function main() {
|
|
|
1431
1525
|
}
|
|
1432
1526
|
if (cmd === "pull")
|
|
1433
1527
|
return handlePull(rest, ctx, env);
|
|
1528
|
+
if (cmd === "init")
|
|
1529
|
+
return handleInit(rest, ctx, env);
|
|
1434
1530
|
if (cmd === "changes" && rest[0] === "decide")
|
|
1435
1531
|
return handleChanges(rest, ctx);
|
|
1436
1532
|
if (cmd === "keys")
|
package/package.json
CHANGED