@shawaze/agentspace 0.3.2 → 0.3.5
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 +22 -0
- package/dist/cli.js +98 -28
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -89,6 +89,28 @@ npx @shawaze/agentspace init
|
|
|
89
89
|
It refuses to write into a non-empty directory unless you pass `--force`, and
|
|
90
90
|
`--dry-run` prints what it would write without touching disk.
|
|
91
91
|
|
|
92
|
+
**Non-interactive** (CI / reproducible scaffolding): skip the wizard and pass a
|
|
93
|
+
JSON config.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx @shawaze/agentspace init --config workspace.json
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```jsonc
|
|
100
|
+
// workspace.json
|
|
101
|
+
{
|
|
102
|
+
"workspaceName": "my-product",
|
|
103
|
+
"shape": "one-product",
|
|
104
|
+
"repos": [
|
|
105
|
+
{ "name": "api", "remote": "https://github.com/me/api.git", "stack": "rails", "role": "backend" },
|
|
106
|
+
{ "name": "web", "remote": "https://github.com/me/web.git", "stack": "nextjs", "role": "frontend" }
|
|
107
|
+
],
|
|
108
|
+
"dependencyOrder": ["api", "web"],
|
|
109
|
+
"pillars": ["manifest", "wiki", "enforcement", "contracts"],
|
|
110
|
+
"enforcement": { "mode": "auto", "warmPages": 5, "warmSessions": 10 }
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
92
114
|
### `agentspace doctor`
|
|
93
115
|
|
|
94
116
|
Mechanical health checks on a generated workspace — manifest validity,
|
package/dist/cli.js
CHANGED
|
@@ -265,7 +265,7 @@ function generateManifest(ctx) {
|
|
|
265
265
|
};
|
|
266
266
|
return [
|
|
267
267
|
{ path: "manifest.yaml", contents: render(MANIFEST_YAML, view) },
|
|
268
|
-
{ path: "clone-repos.sh", contents: render(CLONE_REPOS_SH, view) },
|
|
268
|
+
{ path: "clone-repos.sh", contents: render(CLONE_REPOS_SH, view), mode: 493 },
|
|
269
269
|
{ path: ".gitignore", contents: render(GITIGNORE, view) },
|
|
270
270
|
{ path: "CLAUDE.md", contents: render(ROOT_CLAUDE, view) },
|
|
271
271
|
{ path: "README.md", contents: render(ROOT_README, view) }
|
|
@@ -668,6 +668,7 @@ function claudeCodeAdapter(intents, ctx) {
|
|
|
668
668
|
|
|
669
669
|
// src/fs/writeTree.ts
|
|
670
670
|
import {
|
|
671
|
+
chmod,
|
|
671
672
|
cp,
|
|
672
673
|
mkdir,
|
|
673
674
|
mkdtemp,
|
|
@@ -676,6 +677,7 @@ import {
|
|
|
676
677
|
rm,
|
|
677
678
|
writeFile
|
|
678
679
|
} from "fs/promises";
|
|
680
|
+
import { existsSync as existsSync3 } from "fs";
|
|
679
681
|
import { dirname as dirname2, join as join4 } from "path";
|
|
680
682
|
async function isNonEmptyDir(dir) {
|
|
681
683
|
try {
|
|
@@ -699,16 +701,12 @@ async function writeTree(files, targetDir, opts) {
|
|
|
699
701
|
const dest = join4(temp, file.path);
|
|
700
702
|
await mkdir(dirname2(dest), { recursive: true });
|
|
701
703
|
await writeFile(dest, file.contents);
|
|
704
|
+
if (file.mode !== void 0) await chmod(dest, file.mode);
|
|
702
705
|
}
|
|
703
|
-
if (
|
|
704
|
-
await cp(temp, targetDir, { recursive: true, force: true });
|
|
705
|
-
await rm(temp, { recursive: true, force: true });
|
|
706
|
-
} else if (await isNonEmptyDir(targetDir)) {
|
|
706
|
+
if (existsSync3(targetDir)) {
|
|
707
707
|
await cp(temp, targetDir, { recursive: true, force: true });
|
|
708
708
|
await rm(temp, { recursive: true, force: true });
|
|
709
709
|
} else {
|
|
710
|
-
await rm(targetDir, { recursive: true, force: true }).catch(() => {
|
|
711
|
-
});
|
|
712
710
|
await rename(temp, targetDir);
|
|
713
711
|
}
|
|
714
712
|
} catch (err) {
|
|
@@ -729,20 +727,24 @@ var DEFAULT_ENFORCEMENT = {
|
|
|
729
727
|
};
|
|
730
728
|
|
|
731
729
|
// src/wizard/assemble.ts
|
|
730
|
+
var str = (v) => (v ?? "").trim();
|
|
732
731
|
function assembleConfig(answers) {
|
|
733
732
|
const pillars = ["manifest"];
|
|
734
733
|
if (answers.enableWiki) pillars.push("wiki");
|
|
735
734
|
if (answers.enableEnforcement) pillars.push("enforcement");
|
|
736
735
|
if (answers.enableContracts) pillars.push("contracts");
|
|
737
736
|
return {
|
|
738
|
-
workspaceName: answers.workspaceName
|
|
737
|
+
workspaceName: str(answers.workspaceName),
|
|
739
738
|
shape: answers.shape,
|
|
740
|
-
repos: answers.repos.map((r) =>
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
739
|
+
repos: answers.repos.map((r) => {
|
|
740
|
+
const remote = str(r.remote);
|
|
741
|
+
return {
|
|
742
|
+
name: str(r.name),
|
|
743
|
+
remote: remote === "" ? null : remote,
|
|
744
|
+
stack: str(r.stack) || "generic",
|
|
745
|
+
role: str(r.role)
|
|
746
|
+
};
|
|
747
|
+
}),
|
|
746
748
|
dependencyOrder: shapeHasDependencyOrder(answers.shape) ? answers.dependencyOrder : null,
|
|
747
749
|
pillars,
|
|
748
750
|
enforcement: answers.enableEnforcement ? { ...DEFAULT_ENFORCEMENT } : null
|
|
@@ -873,6 +875,71 @@ async function runWizard() {
|
|
|
873
875
|
});
|
|
874
876
|
}
|
|
875
877
|
|
|
878
|
+
// src/config.ts
|
|
879
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
880
|
+
var SHAPES = [
|
|
881
|
+
"single-repo",
|
|
882
|
+
"one-product",
|
|
883
|
+
"peer-services",
|
|
884
|
+
"library-consumers",
|
|
885
|
+
"unrelated"
|
|
886
|
+
];
|
|
887
|
+
var PILLARS = ["manifest", "wiki", "contracts", "enforcement"];
|
|
888
|
+
var MODES = ["auto", "warn", "block"];
|
|
889
|
+
function validateConfig(raw) {
|
|
890
|
+
if (!raw || typeof raw !== "object") {
|
|
891
|
+
throw new Error("config must be a JSON object");
|
|
892
|
+
}
|
|
893
|
+
const o = raw;
|
|
894
|
+
const workspaceName = typeof o.workspaceName === "string" ? o.workspaceName.trim() : "";
|
|
895
|
+
if (!workspaceName) throw new Error("config.workspaceName is required");
|
|
896
|
+
if (typeof o.shape !== "string" || !SHAPES.includes(o.shape)) {
|
|
897
|
+
throw new Error(`config.shape must be one of: ${SHAPES.join(", ")}`);
|
|
898
|
+
}
|
|
899
|
+
const shape = o.shape;
|
|
900
|
+
if (!Array.isArray(o.repos) || o.repos.length === 0) {
|
|
901
|
+
throw new Error("config.repos must be a non-empty array");
|
|
902
|
+
}
|
|
903
|
+
const repos = o.repos.map((r, i) => {
|
|
904
|
+
const rr = r ?? {};
|
|
905
|
+
const name = typeof rr.name === "string" ? rr.name.trim() : "";
|
|
906
|
+
if (!name) throw new Error(`config.repos[${i}].name is required`);
|
|
907
|
+
const remote = typeof rr.remote === "string" && rr.remote.trim() !== "" ? rr.remote.trim() : null;
|
|
908
|
+
const stack = typeof rr.stack === "string" && rr.stack.trim() ? rr.stack.trim() : "generic";
|
|
909
|
+
const role = typeof rr.role === "string" ? rr.role.trim() : "";
|
|
910
|
+
return { name, remote, stack, role };
|
|
911
|
+
});
|
|
912
|
+
let pillars = Array.isArray(o.pillars) ? o.pillars.filter((p2) => PILLARS.includes(p2)) : ["manifest", "wiki"];
|
|
913
|
+
if (pillars.length === 0) pillars = ["manifest", "wiki"];
|
|
914
|
+
if (!pillars.includes("manifest")) pillars = ["manifest", ...pillars];
|
|
915
|
+
const dependencyOrder = Array.isArray(o.dependencyOrder) ? o.dependencyOrder.filter((s) => typeof s === "string") : null;
|
|
916
|
+
let enforcement = null;
|
|
917
|
+
if (pillars.includes("enforcement")) {
|
|
918
|
+
const e = o.enforcement ?? {};
|
|
919
|
+
enforcement = {
|
|
920
|
+
mode: MODES.includes(e.mode) ? e.mode : DEFAULT_ENFORCEMENT.mode,
|
|
921
|
+
warmPages: typeof e.warmPages === "number" ? e.warmPages : DEFAULT_ENFORCEMENT.warmPages,
|
|
922
|
+
warmSessions: typeof e.warmSessions === "number" ? e.warmSessions : DEFAULT_ENFORCEMENT.warmSessions
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
return { workspaceName, shape, repos, dependencyOrder, pillars, enforcement };
|
|
926
|
+
}
|
|
927
|
+
function loadConfig(path) {
|
|
928
|
+
let raw;
|
|
929
|
+
try {
|
|
930
|
+
raw = readFileSync3(path, "utf8");
|
|
931
|
+
} catch {
|
|
932
|
+
throw new Error(`Cannot read config file: ${path}`);
|
|
933
|
+
}
|
|
934
|
+
let parsed;
|
|
935
|
+
try {
|
|
936
|
+
parsed = JSON.parse(raw);
|
|
937
|
+
} catch {
|
|
938
|
+
throw new Error(`config file is not valid JSON: ${path}`);
|
|
939
|
+
}
|
|
940
|
+
return validateConfig(parsed);
|
|
941
|
+
}
|
|
942
|
+
|
|
876
943
|
// src/commands/init.ts
|
|
877
944
|
function generateWorkspace(config, today) {
|
|
878
945
|
const ctx = buildContext(config, today);
|
|
@@ -895,8 +962,8 @@ async function runInit(config, targetDir, opts) {
|
|
|
895
962
|
await writeTree(files, targetDir, { force: opts.force });
|
|
896
963
|
}
|
|
897
964
|
async function initCommand(opts) {
|
|
898
|
-
const config = await runWizard();
|
|
899
|
-
await runInit(config, process.cwd(), opts);
|
|
965
|
+
const config = opts.configPath ? loadConfig(opts.configPath) : await runWizard();
|
|
966
|
+
await runInit(config, process.cwd(), { force: opts.force, today: opts.today });
|
|
900
967
|
console.log(`
|
|
901
968
|
\u2713 Created ${config.workspaceName} (${config.pillars.join(", ")}).`);
|
|
902
969
|
console.log(" Next: ./clone-repos.sh");
|
|
@@ -909,7 +976,7 @@ async function initCommand(opts) {
|
|
|
909
976
|
|
|
910
977
|
// src/commands/doctor.ts
|
|
911
978
|
import { readFile, readdir as readdir2, stat } from "fs/promises";
|
|
912
|
-
import { existsSync as
|
|
979
|
+
import { existsSync as existsSync4 } from "fs";
|
|
913
980
|
import { join as join5, relative } from "path";
|
|
914
981
|
import { parse as parse2 } from "yaml";
|
|
915
982
|
|
|
@@ -1004,7 +1071,7 @@ async function runChecks(workspaceDir, today, deps = {}) {
|
|
|
1004
1071
|
}
|
|
1005
1072
|
}
|
|
1006
1073
|
}
|
|
1007
|
-
if (
|
|
1074
|
+
if (existsSync4(join5(workspaceDir, "openspec")) && !isOpenspecAvailable()) {
|
|
1008
1075
|
findings.push({
|
|
1009
1076
|
level: "warn",
|
|
1010
1077
|
message: "openspec/ is present but the `openspec` CLI was not found on PATH. Install it (https://github.com/Fission-AI/OpenSpec) and run `openspec update`."
|
|
@@ -1031,25 +1098,28 @@ async function doctorCommand(workspaceDir, today, opts = {}) {
|
|
|
1031
1098
|
}
|
|
1032
1099
|
|
|
1033
1100
|
// src/version.ts
|
|
1034
|
-
var VERSION = "0.3.
|
|
1101
|
+
var VERSION = "0.3.5";
|
|
1035
1102
|
|
|
1036
1103
|
// src/cli.ts
|
|
1037
1104
|
function parseArgs(argv) {
|
|
1038
1105
|
const force = argv.includes("--force");
|
|
1039
1106
|
const lint = argv.includes("--lint");
|
|
1107
|
+
const ci = argv.indexOf("--config");
|
|
1108
|
+
const configPath = ci >= 0 ? argv[ci + 1] : void 0;
|
|
1040
1109
|
const first = argv[0];
|
|
1041
|
-
if (first === "init") return { command: "init", force, lint };
|
|
1042
|
-
if (first === "doctor") return { command: "doctor", force, lint };
|
|
1043
|
-
if (first === "--version" || first === "-v") return { command: "version", force, lint };
|
|
1044
|
-
return { command: "help", force, lint };
|
|
1110
|
+
if (first === "init") return { command: "init", force, lint, configPath };
|
|
1111
|
+
if (first === "doctor") return { command: "doctor", force, lint, configPath };
|
|
1112
|
+
if (first === "--version" || first === "-v") return { command: "version", force, lint, configPath };
|
|
1113
|
+
return { command: "help", force, lint, configPath };
|
|
1045
1114
|
}
|
|
1046
1115
|
var HELP = `agentspace \u2014 scaffold an agent-native multi-repo workspace
|
|
1047
1116
|
|
|
1048
1117
|
Usage:
|
|
1049
|
-
agentspace init [--force]
|
|
1050
|
-
agentspace
|
|
1051
|
-
agentspace --
|
|
1052
|
-
agentspace --
|
|
1118
|
+
agentspace init [--force] Interactively scaffold a workspace in the current dir
|
|
1119
|
+
agentspace init --config <file> Scaffold non-interactively from a JSON config
|
|
1120
|
+
agentspace doctor [--lint] Run mechanical health checks (--lint = JSON output)
|
|
1121
|
+
agentspace --version Print version
|
|
1122
|
+
agentspace --help Show this help
|
|
1053
1123
|
`;
|
|
1054
1124
|
function todayIso() {
|
|
1055
1125
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -1058,7 +1128,7 @@ async function main(argv) {
|
|
|
1058
1128
|
const args = parseArgs(argv);
|
|
1059
1129
|
switch (args.command) {
|
|
1060
1130
|
case "init":
|
|
1061
|
-
await initCommand({ force: args.force, today: todayIso() });
|
|
1131
|
+
await initCommand({ force: args.force, today: todayIso(), configPath: args.configPath });
|
|
1062
1132
|
return 0;
|
|
1063
1133
|
case "doctor":
|
|
1064
1134
|
return doctorCommand(process.cwd(), todayIso(), { lint: args.lint });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shawaze/agentspace",
|
|
3
3
|
"publishConfig": { "access": "public" },
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.5",
|
|
5
5
|
"description": "Scaffold an agent-native multi-repo workspace — keep sibling repos coherent for AI coding agents",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": { "agentspace": "dist/cli.js" },
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"test": "vitest run",
|
|
35
35
|
"test:watch": "vitest",
|
|
36
36
|
"typecheck": "tsc --noEmit",
|
|
37
|
+
"smoke": "node scripts/smoke.mjs",
|
|
37
38
|
"prepublishOnly": "npm run typecheck && npm run build && npm test"
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|