@rryando/arcs 3.1.0 → 3.1.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rryando/arcs",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "ARCS — DAG-based task orchestration for AI agents. Persistent workflow continuity via graph-structured context.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist/",
|
|
12
|
+
"scripts/",
|
|
12
13
|
"templates/",
|
|
13
14
|
"skills/",
|
|
14
15
|
"opencode/"
|
|
@@ -38,8 +39,8 @@
|
|
|
38
39
|
"start": "node dist/index.js",
|
|
39
40
|
"dev": "tsc --watch",
|
|
40
41
|
"test": "vitest run",
|
|
41
|
-
"postinstall": "
|
|
42
|
-
"init": "
|
|
42
|
+
"postinstall": "node scripts/arcs-init.mjs",
|
|
43
|
+
"init": "node dist/index.js init",
|
|
43
44
|
"lint": "biome check src/ test/",
|
|
44
45
|
"lint:fix": "biome check --fix src/ test/",
|
|
45
46
|
"format": "biome format --write src/ test/",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ARCS CLI global registration
|
|
4
|
+
// Usage: node scripts/arcs-init.mjs [-g] [--uninstall]
|
|
5
|
+
// Creates symlink ~/.local/bin/arcs → scripts/arcs-cli.mjs
|
|
6
|
+
|
|
7
|
+
import { existsSync, lstatSync, mkdirSync, symlinkSync, unlinkSync, readlinkSync } from "node:fs";
|
|
8
|
+
import { resolve, dirname } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
// lstatSync-based check: works even for dangling symlinks (existsSync follows the target)
|
|
13
|
+
function linkExists(p) {
|
|
14
|
+
try { lstatSync(p); return true; } catch { return false; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isCommandAvailable(cmd) {
|
|
18
|
+
try {
|
|
19
|
+
execSync(`command -v ${cmd}`, { stdio: "ignore", shell: true });
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const cliPath = resolve(__dirname, "arcs-cli.mjs");
|
|
28
|
+
const binDir = resolve(process.env.HOME || "~", ".local/bin");
|
|
29
|
+
const linkPath = resolve(binDir, "arcs");
|
|
30
|
+
const uninstall = process.argv.includes("--uninstall");
|
|
31
|
+
|
|
32
|
+
if (uninstall) {
|
|
33
|
+
if (linkExists(linkPath)) {
|
|
34
|
+
unlinkSync(linkPath);
|
|
35
|
+
console.log(`Removed: ${linkPath}`);
|
|
36
|
+
} else {
|
|
37
|
+
console.log("Nothing to remove.");
|
|
38
|
+
}
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Ensure bin dir
|
|
43
|
+
if (!existsSync(binDir)) {
|
|
44
|
+
mkdirSync(binDir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Remove stale symlink if exists
|
|
48
|
+
if (linkExists(linkPath)) {
|
|
49
|
+
const current = readlinkSync(linkPath);
|
|
50
|
+
if (current === cliPath) {
|
|
51
|
+
console.log(`Already registered: ${linkPath} → ${cliPath}`);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
unlinkSync(linkPath);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
symlinkSync(cliPath, linkPath);
|
|
58
|
+
console.log(`Registered: ${linkPath} → ${cliPath}`);
|
|
59
|
+
|
|
60
|
+
// PATH check
|
|
61
|
+
const pathDirs = (process.env.PATH || "").split(":");
|
|
62
|
+
if (!pathDirs.includes(binDir)) {
|
|
63
|
+
console.warn(`\nWARNING: ${binDir} is not in PATH.`);
|
|
64
|
+
console.warn(`Add to your shell rc: export PATH="$HOME/.local/bin:$PATH"`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(`\nUsage: arcs <command> [args]`);
|
|
68
|
+
console.log(`Commands: context, task, plan, knowledge, search, diagram, batch, validate`);
|
|
69
|
+
|
|
70
|
+
// Dependency checks
|
|
71
|
+
console.log("");
|
|
72
|
+
if (!isCommandAvailable("gh")) {
|
|
73
|
+
console.warn(`WARNING: gh (GitHub CLI) not found.`);
|
|
74
|
+
console.warn(` Skills like deep-pr-review require it: https://cli.github.com/`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!isCommandAvailable("rtk")) {
|
|
78
|
+
console.warn(`WARNING: rtk not found.`);
|
|
79
|
+
console.warn(` RTK improves AI command usage tracking: https://github.com/rtk-ai/rtk`);
|
|
80
|
+
console.warn(` Install: rtk init -g (or rtk init -g --opencode for OpenCode)`);
|
|
81
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, relative, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
normalizeRelativePath,
|
|
7
|
+
listDeclaredFiles,
|
|
8
|
+
validateDeclaredPath,
|
|
9
|
+
} from "./lib/bundle-helpers.mjs";
|
|
10
|
+
|
|
11
|
+
const repoRoot = resolve(import.meta.dirname, "..");
|
|
12
|
+
const defaultManifestPath = resolve(repoRoot, "opencode/arcs/bundle-runtime.json");
|
|
13
|
+
const defaultOutputRoot = resolve(repoRoot, "opencode/arcs");
|
|
14
|
+
// Files that are repo-authored and must not be pruned. The repo bundle
|
|
15
|
+
// directory IS the source of truth — there is no external mirror.
|
|
16
|
+
const preservedOutputFiles = new Set([
|
|
17
|
+
"manifest.json",
|
|
18
|
+
"bundle-runtime.json",
|
|
19
|
+
".opencode/plugins/arcs.js",
|
|
20
|
+
// ARCS-native skills (authored in this repo, no upstream source)
|
|
21
|
+
// init-project skill — ARCS-native (mirrors orchestrator INIT workflow with
|
|
22
|
+
// graphify sub-flow, typed-agent dispatch, knowledge categories).
|
|
23
|
+
"skills/init-project/SKILL.md",
|
|
24
|
+
// Caveman commit skill — adapted from https://github.com/JuliusBrussee/caveman (MIT).
|
|
25
|
+
"skills/caveman-commit/SKILL.md",
|
|
26
|
+
// Agent prompt files (repo-authored, referenced via {file:} in manifest.json)
|
|
27
|
+
"prompts/software-engineer.txt",
|
|
28
|
+
"prompts/tech-architect.txt",
|
|
29
|
+
"prompts/qa-analyst.txt",
|
|
30
|
+
"prompts/oncall-ops.txt",
|
|
31
|
+
"prompts/arcs-docs.txt",
|
|
32
|
+
"prompts/system-architect.txt",
|
|
33
|
+
"prompts/code-reviewer.txt",
|
|
34
|
+
"prompts/docs-researcher.txt",
|
|
35
|
+
"prompts/devil-advocate.txt",
|
|
36
|
+
// Orchestrator prompt files — generated from src/cli/arcs-orchestrate*.ts during
|
|
37
|
+
// bundle build (see generateOrchestratorPrompts() below). TS modules remain the
|
|
38
|
+
// canonical source; these .txt files are committed mirrors so the bundle is
|
|
39
|
+
// self-describing and all prompts live in one directory.
|
|
40
|
+
"prompts/arcs-orchestrate.txt",
|
|
41
|
+
"prompts/arcs-orchestrate-caveman.txt",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
function ensureParentDirectory(filePath) {
|
|
45
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pruneUndeclaredFiles(rootPath, allowedFiles) {
|
|
49
|
+
for (const entry of readdirSync(rootPath, { withFileTypes: true })) {
|
|
50
|
+
const entryPath = resolve(rootPath, entry.name);
|
|
51
|
+
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
pruneUndeclaredFiles(entryPath, allowedFiles);
|
|
54
|
+
|
|
55
|
+
if (readdirSync(entryPath).length === 0) {
|
|
56
|
+
rmSync(entryPath, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const relativePath = normalizeRelativePath(relative(defaultOutputRootCurrent, entryPath));
|
|
63
|
+
if (!allowedFiles.has(relativePath)) {
|
|
64
|
+
rmSync(entryPath, { force: true });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let defaultOutputRootCurrent = defaultOutputRoot;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generates the ARCS Orchestrator and ARCS Caveman prompt .txt files into
|
|
73
|
+
* <outputRoot>/prompts/. The TypeScript modules src/cli/arcs-orchestrate.ts
|
|
74
|
+
* and src/cli/arcs-orchestrate-caveman.ts remain the canonical source; these
|
|
75
|
+
* .txt files are committed mirrors so the bundle is self-describing alongside
|
|
76
|
+
* the static sub-agent prompts.
|
|
77
|
+
*
|
|
78
|
+
* Requires `tsc` to have run first (dist/cli/arcs-orchestrate.js must exist).
|
|
79
|
+
* package.json's build:opencode-bundle chains `tsc` before this script.
|
|
80
|
+
*/
|
|
81
|
+
async function generateOrchestratorPrompts(outputRoot) {
|
|
82
|
+
const orchestrateModulePath = resolve(repoRoot, "dist/cli/arcs-orchestrate.js");
|
|
83
|
+
const cavemanModulePath = resolve(repoRoot, "dist/cli/arcs-orchestrate-caveman.js");
|
|
84
|
+
|
|
85
|
+
if (!existsSync(orchestrateModulePath) || !existsSync(cavemanModulePath)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Compiled orchestrator modules missing. Run \`npm run build\` before bundle build.\n` +
|
|
88
|
+
` Expected: ${orchestrateModulePath}\n` +
|
|
89
|
+
` Expected: ${cavemanModulePath}`, );
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const orchestrateModule = await import(pathToFileURL(orchestrateModulePath).href);
|
|
93
|
+
const cavemanModule = await import(pathToFileURL(cavemanModulePath).href);
|
|
94
|
+
|
|
95
|
+
const orchestrateText = orchestrateModule.ORCHESTRATE_PROMPT_TEXT;
|
|
96
|
+
const cavemanText = cavemanModule.ORCHESTRATE_CAVEMAN_PROMPT_TEXT;
|
|
97
|
+
|
|
98
|
+
if (typeof orchestrateText !== "string" || orchestrateText.length === 0) {
|
|
99
|
+
throw new Error("ORCHESTRATE_PROMPT_TEXT not exported as non-empty string");
|
|
100
|
+
}
|
|
101
|
+
if (typeof cavemanText !== "string" || cavemanText.length === 0) {
|
|
102
|
+
throw new Error("ORCHESTRATE_CAVEMAN_PROMPT_TEXT not exported as non-empty string");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const promptsDir = resolve(outputRoot, "prompts");
|
|
106
|
+
mkdirSync(promptsDir, { recursive: true });
|
|
107
|
+
|
|
108
|
+
const orchestratePath = resolve(promptsDir, "arcs-orchestrate.txt");
|
|
109
|
+
const cavemanPath = resolve(promptsDir, "arcs-orchestrate-caveman.txt");
|
|
110
|
+
|
|
111
|
+
// Banner prepended to every generated prompt file. Uses HTML comment syntax
|
|
112
|
+
// so it's invisible when rendered as markdown but obvious to anyone opening
|
|
113
|
+
// the .txt directly. LLMs treat HTML comments as out-of-band metadata, so
|
|
114
|
+
// the banner does not pollute the prompt's actionable instructions.
|
|
115
|
+
const banner = (sourceFile) =>
|
|
116
|
+
`<!--\n` +
|
|
117
|
+
` AUTO-GENERATED — DO NOT EDIT.\n` +
|
|
118
|
+
` Source of truth: ${sourceFile}\n` +
|
|
119
|
+
` Regenerate: npm run build:opencode-bundle\n` +
|
|
120
|
+
` Edits to this file will be overwritten on the next build.\n` +
|
|
121
|
+
`-->\n\n`;
|
|
122
|
+
|
|
123
|
+
writeFileSync(
|
|
124
|
+
orchestratePath,
|
|
125
|
+
`${banner("src/cli/arcs-orchestrate.ts")}${orchestrateText}\n`,
|
|
126
|
+
"utf-8",
|
|
127
|
+
);
|
|
128
|
+
writeFileSync(
|
|
129
|
+
cavemanPath,
|
|
130
|
+
`${banner("src/cli/arcs-orchestrate-caveman.ts")}${cavemanText}\n`,
|
|
131
|
+
"utf-8",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function main() {
|
|
136
|
+
const manifestPath = process.env.ARCS_BUNDLE_RUNTIME_MANIFEST
|
|
137
|
+
? resolve(repoRoot, process.env.ARCS_BUNDLE_RUNTIME_MANIFEST)
|
|
138
|
+
: defaultManifestPath;
|
|
139
|
+
const runtimeManifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
140
|
+
const outputRoot = process.env.ARCS_BUNDLE_OUTPUT_ROOT
|
|
141
|
+
? resolve(repoRoot, process.env.ARCS_BUNDLE_OUTPUT_ROOT)
|
|
142
|
+
: defaultOutputRoot;
|
|
143
|
+
|
|
144
|
+
const declaredFiles = listDeclaredFiles(runtimeManifest);
|
|
145
|
+
const allowedOutputFiles = new Set([
|
|
146
|
+
...declaredFiles.map((entry) => entry.declaredPath),
|
|
147
|
+
...preservedOutputFiles,
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
defaultOutputRootCurrent = outputRoot;
|
|
151
|
+
|
|
152
|
+
// Validate that every manifest-declared file already exists in the bundle.
|
|
153
|
+
// The bundle directory IS the source of truth — files are authored here,
|
|
154
|
+
// not copied from anywhere external.
|
|
155
|
+
for (const { declaredPath, validationRoot } of declaredFiles) {
|
|
156
|
+
const relativePath = validateDeclaredPath(declaredPath, outputRoot, validationRoot);
|
|
157
|
+
const outputPath = resolve(outputRoot, relativePath);
|
|
158
|
+
if (!existsSync(outputPath)) {
|
|
159
|
+
throw new Error(`Missing declared bundle file: ${relativePath} (${outputPath})`);
|
|
160
|
+
}
|
|
161
|
+
ensureParentDirectory(outputPath);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
mkdirSync(outputRoot, { recursive: true });
|
|
165
|
+
pruneUndeclaredFiles(outputRoot, allowedOutputFiles);
|
|
166
|
+
|
|
167
|
+
// Generate orchestrator prompt mirrors after prune so they always end up
|
|
168
|
+
// on disk fresh from the canonical TS sources.
|
|
169
|
+
await generateOrchestratorPrompts(outputRoot);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
await main();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
176
|
+
process.stderr.write(`${message}\n`);
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Deploy opencode ARCS bundle bundle from repo to user config.
|
|
3
|
+
//
|
|
4
|
+
// Direction: repo → config ONLY. Never writes config → repo.
|
|
5
|
+
//
|
|
6
|
+
// Env vars:
|
|
7
|
+
// DEPLOY_BUNDLE_ROOT — override bundle root (default: opencode/arcs)
|
|
8
|
+
// DEPLOY_CONFIG_ROOT — override config root (default: ~/.config/opencode)
|
|
9
|
+
// DEPLOY_DRY_RUN — "false" to actually copy; anything else = dry-run (default: dry-run)
|
|
10
|
+
//
|
|
11
|
+
// Outputs JSON to stdout: DeployResult
|
|
12
|
+
// Exit code: 0 on success, 1 on error.
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
copyFileSync,
|
|
16
|
+
existsSync,
|
|
17
|
+
lstatSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
readdirSync,
|
|
20
|
+
readFileSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
} from "node:fs";
|
|
24
|
+
import { homedir } from "node:os";
|
|
25
|
+
import { dirname, relative, resolve } from "node:path";
|
|
26
|
+
|
|
27
|
+
const repoRoot = resolve(import.meta.dirname, "..");
|
|
28
|
+
const defaultBundleRoot = resolve(repoRoot, "opencode/arcs");
|
|
29
|
+
const defaultConfigRoot = resolve(homedir(), ".config/opencode");
|
|
30
|
+
|
|
31
|
+
const bundleRoot = process.env.DEPLOY_BUNDLE_ROOT
|
|
32
|
+
? resolve(repoRoot, process.env.DEPLOY_BUNDLE_ROOT)
|
|
33
|
+
: defaultBundleRoot;
|
|
34
|
+
const configRoot = process.env.DEPLOY_CONFIG_ROOT
|
|
35
|
+
? resolve(repoRoot, process.env.DEPLOY_CONFIG_ROOT)
|
|
36
|
+
: defaultConfigRoot;
|
|
37
|
+
// Dry-run by default. Only DEPLOY_DRY_RUN=false (exact string) triggers real writes.
|
|
38
|
+
const dryRun = process.env.DEPLOY_DRY_RUN !== "false";
|
|
39
|
+
|
|
40
|
+
function listAllFiles(rootPath, currentPath = rootPath) {
|
|
41
|
+
if (!existsSync(currentPath)) return [];
|
|
42
|
+
const entries = readdirSync(currentPath, { withFileTypes: true });
|
|
43
|
+
return entries.flatMap((entry) => {
|
|
44
|
+
const entryPath = resolve(currentPath, entry.name);
|
|
45
|
+
if (entry.isDirectory()) return listAllFiles(rootPath, entryPath);
|
|
46
|
+
return [relative(rootPath, entryPath).replace(/\\/g, "/")];
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ensureParentDir(filePath) {
|
|
51
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function main() {
|
|
55
|
+
const manifestPath = resolve(bundleRoot, "manifest.json");
|
|
56
|
+
if (!existsSync(manifestPath)) {
|
|
57
|
+
throw new Error(`manifest.json not found at ${manifestPath}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
61
|
+
|
|
62
|
+
// Build mapping: config-relative path → bundle-absolute path
|
|
63
|
+
const deployMap = new Map();
|
|
64
|
+
|
|
65
|
+
// Skills: bundle skills/<name>/<file> → config skills/arcs/<name>/<file>
|
|
66
|
+
const skillsSourceDir = resolve(bundleRoot, manifest.skills.source);
|
|
67
|
+
if (existsSync(skillsSourceDir)) {
|
|
68
|
+
const skillFiles = listAllFiles(skillsSourceDir);
|
|
69
|
+
for (const file of skillFiles) {
|
|
70
|
+
const configRelative = `${manifest.skills.destination}/${file}`;
|
|
71
|
+
const bundleAbsolute = resolve(skillsSourceDir, file);
|
|
72
|
+
deployMap.set(configRelative, bundleAbsolute);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Plugin
|
|
77
|
+
if (manifest.plugin && manifest.plugin.source) {
|
|
78
|
+
const pluginSource = resolve(bundleRoot, manifest.plugin.source);
|
|
79
|
+
if (existsSync(pluginSource)) {
|
|
80
|
+
deployMap.set(manifest.plugin.destination, pluginSource);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Agents (sub-agent prompts) — bundle prompts/<file> → config prompts/<file>
|
|
85
|
+
for (const agent of manifest.agents ?? []) {
|
|
86
|
+
const agentSource = resolve(bundleRoot, agent.source);
|
|
87
|
+
if (existsSync(agentSource)) {
|
|
88
|
+
deployMap.set(agent.destination, agentSource);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Pass 1: Determine file states (detection only — no writes yet)
|
|
93
|
+
const filesAdded = [];
|
|
94
|
+
const filesChanged = [];
|
|
95
|
+
const filesUnchanged = [];
|
|
96
|
+
let restartRequired = false;
|
|
97
|
+
|
|
98
|
+
for (const [configRelative, bundleAbsolute] of deployMap) {
|
|
99
|
+
const configAbsolute = resolve(configRoot, configRelative);
|
|
100
|
+
const sourceContent = readFileSync(bundleAbsolute, "utf-8");
|
|
101
|
+
|
|
102
|
+
const isNew = !existsSync(configAbsolute);
|
|
103
|
+
const isChanged =
|
|
104
|
+
!isNew && readFileSync(configAbsolute, "utf-8") !== sourceContent;
|
|
105
|
+
|
|
106
|
+
if (isNew) {
|
|
107
|
+
filesAdded.push(configRelative);
|
|
108
|
+
} else if (isChanged) {
|
|
109
|
+
filesChanged.push(configRelative);
|
|
110
|
+
} else {
|
|
111
|
+
filesUnchanged.push(configRelative);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Plugin change/add → restart required
|
|
115
|
+
if (configRelative === manifest.plugin?.destination && (isNew || isChanged)) {
|
|
116
|
+
restartRequired = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Detect files to remove: files in owned paths in config that are NOT in deployMap
|
|
121
|
+
const filesRemoved = [];
|
|
122
|
+
for (const ownedPath of manifest.ownedPaths ?? []) {
|
|
123
|
+
const ownedAbsolute = resolve(configRoot, ownedPath);
|
|
124
|
+
if (!existsSync(ownedAbsolute)) continue;
|
|
125
|
+
|
|
126
|
+
// Owned path may be a file or directory
|
|
127
|
+
if (!lstatSync(ownedAbsolute).isDirectory()) {
|
|
128
|
+
if (!deployMap.has(ownedPath)) {
|
|
129
|
+
filesRemoved.push(ownedPath);
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const existingFiles = listAllFiles(ownedAbsolute);
|
|
135
|
+
for (const file of existingFiles) {
|
|
136
|
+
const configRelative = `${ownedPath}/${file}`;
|
|
137
|
+
if (!deployMap.has(configRelative)) {
|
|
138
|
+
filesRemoved.push(configRelative);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Pass 2: Apply writes (only when not dry-run)
|
|
144
|
+
if (!dryRun) {
|
|
145
|
+
// Clean-delete the skills directory before copying to guarantee a fresh install.
|
|
146
|
+
// Prevents residual files from renamed/removed skills surviving across deploys.
|
|
147
|
+
const skillsDest = resolve(configRoot, manifest.skills.destination);
|
|
148
|
+
if (existsSync(skillsDest)) {
|
|
149
|
+
rmSync(skillsDest, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Write all files from deployMap (recreates skills dir + copies plugin + agents)
|
|
153
|
+
for (const [configRelative, bundleAbsolute] of deployMap) {
|
|
154
|
+
const configAbsolute = resolve(configRoot, configRelative);
|
|
155
|
+
ensureParentDir(configAbsolute);
|
|
156
|
+
copyFileSync(bundleAbsolute, configAbsolute);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Remove orphans from other owned paths (skills dir already cleared above; force: true
|
|
160
|
+
// makes this a no-op for any skills paths that were already wiped)
|
|
161
|
+
for (const fileToRemove of filesRemoved) {
|
|
162
|
+
rmSync(resolve(configRoot, fileToRemove), { force: true });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// After successful deploy, ensure arcs CLI is globally registered
|
|
167
|
+
if (!dryRun) {
|
|
168
|
+
try {
|
|
169
|
+
const { execFileSync } = await import("node:child_process");
|
|
170
|
+
const initScript = resolve(repoRoot, "scripts/arcs-init.mjs");
|
|
171
|
+
if (existsSync(initScript)) {
|
|
172
|
+
execFileSync(process.execPath, [initScript], { stdio: "pipe" });
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// Non-fatal: CLI registration is a convenience, not a requirement
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const result = {
|
|
180
|
+
dryRun,
|
|
181
|
+
source: bundleRoot,
|
|
182
|
+
destination: configRoot,
|
|
183
|
+
filesAdded,
|
|
184
|
+
filesChanged,
|
|
185
|
+
filesRemoved,
|
|
186
|
+
filesUnchanged,
|
|
187
|
+
restartRequired,
|
|
188
|
+
cliRegistered: !dryRun,
|
|
189
|
+
...(restartRequired && {
|
|
190
|
+
restartGuidance: "Plugin file changed. Restart opencode for changes to take effect.",
|
|
191
|
+
}),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
main();
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
201
|
+
process.stderr.write(`${message}\n`);
|
|
202
|
+
process.exitCode = 1;
|
|
203
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
export function normalizeRelativePath(filePath) {
|
|
4
|
+
return filePath.replace(/\\/g, "/");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function looksWindowsAbsolute(filePath) {
|
|
8
|
+
return /^[A-Za-z]:[\\/]/.test(filePath) || /^\\\\/.test(filePath);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function assertNoReservedPathSegments(candidatePath, reportedPath = candidatePath) {
|
|
12
|
+
const normalizedPath = normalizeRelativePath(candidatePath);
|
|
13
|
+
|
|
14
|
+
if (!normalizedPath || normalizedPath === ".") {
|
|
15
|
+
throw new Error(`Invalid declared runtime path: ${reportedPath}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (isAbsolute(normalizedPath) || looksWindowsAbsolute(normalizedPath)) {
|
|
19
|
+
throw new Error(`Invalid declared runtime path: ${reportedPath}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const segment of normalizedPath.split("/")) {
|
|
23
|
+
if (!segment || segment === "." || segment === "..") {
|
|
24
|
+
throw new Error(`Invalid declared runtime path: ${reportedPath}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return normalizedPath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function assertSafeOutputPath(candidatePath, reportedPath = candidatePath) {
|
|
32
|
+
const normalizedPath = normalizeRelativePath(candidatePath);
|
|
33
|
+
|
|
34
|
+
if (!normalizedPath || normalizedPath === ".") {
|
|
35
|
+
throw new Error(`Invalid declared runtime path: ${reportedPath}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isAbsolute(normalizedPath) || looksWindowsAbsolute(normalizedPath)) {
|
|
39
|
+
throw new Error(`Invalid declared runtime path: ${reportedPath}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return normalizedPath;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function assertPathWithinCategoryRoot(candidatePath, categoryRoot, reportedPath = candidatePath) {
|
|
46
|
+
const normalizedPath = assertSafeOutputPath(candidatePath, reportedPath);
|
|
47
|
+
const normalizedCategoryRoot = assertNoReservedPathSegments(categoryRoot, reportedPath);
|
|
48
|
+
const categoryRelativePath = normalizeRelativePath(
|
|
49
|
+
relative(normalizedCategoryRoot, normalizedPath),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
normalizedPath !== normalizedCategoryRoot &&
|
|
54
|
+
(categoryRelativePath.startsWith("../") || categoryRelativePath === "..")
|
|
55
|
+
) {
|
|
56
|
+
throw new Error(`Invalid declared runtime path: ${reportedPath}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return normalizedPath;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function assertTopLevelMarkdownFile(candidatePath, categoryRoot, reportedPath = candidatePath) {
|
|
63
|
+
const normalizedPath = assertPathWithinCategoryRoot(candidatePath, categoryRoot, reportedPath);
|
|
64
|
+
const normalizedCategoryRoot = assertNoReservedPathSegments(categoryRoot, reportedPath);
|
|
65
|
+
const categoryRelativePath = normalizeRelativePath(
|
|
66
|
+
relative(normalizedCategoryRoot, normalizedPath),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
!categoryRelativePath ||
|
|
71
|
+
categoryRelativePath.includes("/") ||
|
|
72
|
+
!categoryRelativePath.endsWith(".md")
|
|
73
|
+
) {
|
|
74
|
+
throw new Error(`Invalid declared runtime path: ${reportedPath}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return normalizedPath;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function listDeclaredFiles(runtimeManifest) {
|
|
81
|
+
const declaredFiles = [];
|
|
82
|
+
|
|
83
|
+
for (const [skillName, skillFiles] of Object.entries(runtimeManifest.skills ?? {})) {
|
|
84
|
+
for (const skillFile of skillFiles) {
|
|
85
|
+
const normalizedSkillName = normalizeRelativePath(skillName);
|
|
86
|
+
const normalizedSkillFile = normalizeRelativePath(skillFile);
|
|
87
|
+
const declaredPath = `skills/${normalizedSkillName}/${normalizedSkillFile}`;
|
|
88
|
+
|
|
89
|
+
assertNoReservedPathSegments(normalizedSkillName, declaredPath);
|
|
90
|
+
assertNoReservedPathSegments(normalizedSkillFile, declaredPath);
|
|
91
|
+
assertSafeOutputPath(declaredPath);
|
|
92
|
+
|
|
93
|
+
declaredFiles.push({
|
|
94
|
+
declaredPath,
|
|
95
|
+
validationRoot: `skills/${normalizedSkillName}`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const agentFile of runtimeManifest.agents ?? []) {
|
|
101
|
+
const declaredPath = assertTopLevelMarkdownFile(
|
|
102
|
+
assertNoReservedPathSegments(normalizeRelativePath(agentFile)),
|
|
103
|
+
"agents",
|
|
104
|
+
agentFile,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
declaredFiles.push({
|
|
108
|
+
declaredPath,
|
|
109
|
+
validationRoot: "agents",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const pluginFile of runtimeManifest.plugin ?? []) {
|
|
114
|
+
const declaredPath = assertPathWithinCategoryRoot(
|
|
115
|
+
assertNoReservedPathSegments(normalizeRelativePath(pluginFile)),
|
|
116
|
+
".opencode/plugins",
|
|
117
|
+
pluginFile,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
declaredFiles.push({
|
|
121
|
+
declaredPath,
|
|
122
|
+
validationRoot: ".opencode/plugins",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return declaredFiles;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function listSourceSkillNames(sourceRoot) {
|
|
130
|
+
// DEPRECATED: source-root mirroring removed. The repo bundle directory is
|
|
131
|
+
// the source of truth. This export is retained as a no-op for any external
|
|
132
|
+
// consumer (e.g. ad-hoc scripts) and can be deleted once no callers remain.
|
|
133
|
+
void sourceRoot;
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function listSourceAgentPaths(sourceRoot) {
|
|
138
|
+
// DEPRECATED: see listSourceSkillNames.
|
|
139
|
+
void sourceRoot;
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function assertSourceParity(runtimeManifest, sourceRoot, arcsNativeSkillNames) {
|
|
144
|
+
// DEPRECATED: source-root mirroring removed. The repo bundle directory is
|
|
145
|
+
// the source of truth — there is no external mirror to assert parity with.
|
|
146
|
+
// Retained as a no-op so any external caller continues to import cleanly.
|
|
147
|
+
void runtimeManifest;
|
|
148
|
+
void sourceRoot;
|
|
149
|
+
void arcsNativeSkillNames;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function validateDeclaredPath(relativePath, outputRoot, validationRoot) {
|
|
153
|
+
const normalizedPath = assertSafeOutputPath(relativePath);
|
|
154
|
+
const normalizedValidationRoot = assertSafeOutputPath(validationRoot, relativePath);
|
|
155
|
+
|
|
156
|
+
const outputPath = resolve(outputRoot, normalizedPath);
|
|
157
|
+
const outputRelativePath = normalizeRelativePath(relative(outputRoot, outputPath));
|
|
158
|
+
|
|
159
|
+
if (outputRelativePath.startsWith("../") || outputRelativePath === "..") {
|
|
160
|
+
throw new Error(`Invalid declared runtime path: ${relativePath}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const validationRelativePath = normalizeRelativePath(
|
|
164
|
+
relative(normalizedValidationRoot, outputRelativePath),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (validationRelativePath.startsWith("../") || validationRelativePath === "..") {
|
|
168
|
+
throw new Error(`Invalid declared runtime path: ${relativePath}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return normalizedPath;
|
|
172
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Bundle linter — detects drift/issues in the opencode ARCS bundle without
|
|
3
|
+
// overwriting anything. The repo bundle directory is the source of truth.
|
|
4
|
+
//
|
|
5
|
+
// Env vars:
|
|
6
|
+
// BUNDLE_LINT_BUNDLE_ROOT — override bundle root (default: opencode/arcs)
|
|
7
|
+
//
|
|
8
|
+
// Outputs JSON to stdout: { issues: [...], summary: { errors, warnings } }
|
|
9
|
+
// Exit code: 0 if no errors, 1 if errors found.
|
|
10
|
+
|
|
11
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { relative, resolve } from "node:path";
|
|
13
|
+
|
|
14
|
+
const repoRoot = resolve(import.meta.dirname, "..");
|
|
15
|
+
const defaultBundleRoot = resolve(repoRoot, "opencode/arcs");
|
|
16
|
+
|
|
17
|
+
const bundleRoot = process.env.BUNDLE_LINT_BUNDLE_ROOT
|
|
18
|
+
? resolve(repoRoot, process.env.BUNDLE_LINT_BUNDLE_ROOT)
|
|
19
|
+
: defaultBundleRoot;
|
|
20
|
+
|
|
21
|
+
// Preserved files that are repo-authored, not sourced from manifest.
|
|
22
|
+
const preservedFiles = new Set([
|
|
23
|
+
"manifest.json",
|
|
24
|
+
"bundle-runtime.json",
|
|
25
|
+
".opencode/plugins/arcs.js",
|
|
26
|
+
"skills/loop/SKILL.md",
|
|
27
|
+
"skills/caveman-commit/SKILL.md",
|
|
28
|
+
"skills/caveman-review/SKILL.md",
|
|
29
|
+
"skills/init-project/SKILL.md",
|
|
30
|
+
// Sub-agent prompt files (repo-authored, referenced from manifest.json
|
|
31
|
+
// requiredMerges, not from bundle-runtime.json's `agents` array).
|
|
32
|
+
"prompts/software-engineer.txt",
|
|
33
|
+
"prompts/tech-architect.txt",
|
|
34
|
+
"prompts/qa-analyst.txt",
|
|
35
|
+
"prompts/oncall-ops.txt",
|
|
36
|
+
"prompts/arcs-docs.txt",
|
|
37
|
+
"prompts/system-architect.txt",
|
|
38
|
+
"prompts/code-reviewer.txt",
|
|
39
|
+
"prompts/docs-researcher.txt",
|
|
40
|
+
// Orchestrator prompt files — generated from src/cli/arcs-orchestrate*.ts
|
|
41
|
+
// by build-opencode-bundle.mjs. Committed mirrors so the bundle is
|
|
42
|
+
// self-describing alongside the static sub-agent prompts.
|
|
43
|
+
"prompts/arcs-orchestrate.txt",
|
|
44
|
+
"prompts/arcs-orchestrate-caveman.txt",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/** @type {Array<{severity: 'error'|'warning', kind: string, message: string, file?: string, repair?: string}>} */const issues = [];
|
|
48
|
+
|
|
49
|
+
function addIssue(severity, kind, message, file, repair) {
|
|
50
|
+
const issue = { severity, kind, message };
|
|
51
|
+
if (file) issue.file = file;
|
|
52
|
+
if (repair) issue.repair = repair;
|
|
53
|
+
issues.push(issue);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function listAllFiles(rootPath, currentPath = rootPath) {
|
|
57
|
+
if (!existsSync(currentPath)) return [];
|
|
58
|
+
const entries = readdirSync(currentPath, { withFileTypes: true });
|
|
59
|
+
return entries.flatMap((entry) => {
|
|
60
|
+
const entryPath = resolve(currentPath, entry.name);
|
|
61
|
+
if (entry.isDirectory()) return listAllFiles(rootPath, entryPath);
|
|
62
|
+
return [relative(rootPath, entryPath).replace(/\\/g, "/")];
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- Read manifest ---
|
|
67
|
+
const manifestPath = resolve(bundleRoot, "bundle-runtime.json");
|
|
68
|
+
if (!existsSync(manifestPath)) {
|
|
69
|
+
addIssue("error", "manifest-missing", `bundle-runtime.json not found at ${manifestPath}`);
|
|
70
|
+
output();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
74
|
+
|
|
75
|
+
// --- Check 1: Every manifest-declared bundled file exists ---
|
|
76
|
+
const declaredFiles = new Set();
|
|
77
|
+
|
|
78
|
+
for (const [skillName, skillFiles] of Object.entries(manifest.skills ?? {})) {
|
|
79
|
+
for (const skillFile of skillFiles) {
|
|
80
|
+
const relativePath = `skills/${skillName}/${skillFile}`;
|
|
81
|
+
declaredFiles.add(relativePath);
|
|
82
|
+
if (!existsSync(resolve(bundleRoot, relativePath))) {
|
|
83
|
+
addIssue(
|
|
84
|
+
"error",
|
|
85
|
+
"missing-declared-file",
|
|
86
|
+
`Manifest declares ${relativePath} but file is missing`,
|
|
87
|
+
relativePath,
|
|
88
|
+
`Run bundle build or add the file`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const agentFile of manifest.agents ?? []) {
|
|
95
|
+
declaredFiles.add(agentFile);
|
|
96
|
+
if (!existsSync(resolve(bundleRoot, agentFile))) {
|
|
97
|
+
addIssue("error", "missing-declared-file", `Manifest declares ${agentFile} but file is missing`, agentFile);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const pluginFile of manifest.plugin ?? []) {
|
|
102
|
+
declaredFiles.add(pluginFile);
|
|
103
|
+
if (!existsSync(resolve(bundleRoot, pluginFile))) {
|
|
104
|
+
addIssue("error", "missing-declared-file", `Manifest declares ${pluginFile} but file is missing`, pluginFile);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Check 2: No extra undeclared files in bundle ---
|
|
109
|
+
const allBundleFiles = listAllFiles(bundleRoot);
|
|
110
|
+
const allowedFiles = new Set([...declaredFiles, ...preservedFiles]);
|
|
111
|
+
|
|
112
|
+
for (const file of allBundleFiles) {
|
|
113
|
+
if (!allowedFiles.has(file)) {
|
|
114
|
+
addIssue(
|
|
115
|
+
"warning",
|
|
116
|
+
"undeclared-file",
|
|
117
|
+
`File ${file} exists in bundle but is not declared in manifest`,
|
|
118
|
+
file,
|
|
119
|
+
`Remove the file or add it to bundle-runtime.json`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- Check 3: Every skill includes SKILL.md ---
|
|
125
|
+
for (const [skillName, skillFiles] of Object.entries(manifest.skills ?? {})) {
|
|
126
|
+
if (!skillFiles.includes("SKILL.md")) {
|
|
127
|
+
addIssue(
|
|
128
|
+
"error",
|
|
129
|
+
"skill-missing-entry",
|
|
130
|
+
`Skill "${skillName}" does not include SKILL.md in its file list`,
|
|
131
|
+
`skills/${skillName}/SKILL.md`,
|
|
132
|
+
`Add "SKILL.md" to the skill's file array in bundle-runtime.json`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- Check 4: .mjs scripts declared should be parseable ---
|
|
138
|
+
for (const [skillName, skillFiles] of Object.entries(manifest.skills ?? {})) {
|
|
139
|
+
for (const skillFile of skillFiles) {
|
|
140
|
+
if (skillFile.endsWith(".mjs")) {
|
|
141
|
+
const filePath = resolve(bundleRoot, `skills/${skillName}/${skillFile}`);
|
|
142
|
+
if (existsSync(filePath)) {
|
|
143
|
+
// Quick syntax check — try to parse as module
|
|
144
|
+
try {
|
|
145
|
+
const content = readFileSync(filePath, "utf-8");
|
|
146
|
+
// Basic check: not empty
|
|
147
|
+
if (content.trim().length === 0) {
|
|
148
|
+
addIssue(
|
|
149
|
+
"warning",
|
|
150
|
+
"empty-script",
|
|
151
|
+
`Bundled script skills/${skillName}/${skillFile} is empty`,
|
|
152
|
+
`skills/${skillName}/${skillFile}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
addIssue(
|
|
157
|
+
"error",
|
|
158
|
+
"unreadable-script",
|
|
159
|
+
`Cannot read bundled script: ${err.message}`,
|
|
160
|
+
`skills/${skillName}/${skillFile}`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --- Check 5: arcs-dashboard/package.json type field ---
|
|
169
|
+
const dashboardPkgPath = resolve(bundleRoot, "skills/arcs-dashboard/package.json");
|
|
170
|
+
if (existsSync(dashboardPkgPath)) {
|
|
171
|
+
try {
|
|
172
|
+
const pkg = JSON.parse(readFileSync(dashboardPkgPath, "utf-8"));
|
|
173
|
+
if (!pkg.type) {
|
|
174
|
+
addIssue(
|
|
175
|
+
"error",
|
|
176
|
+
"package-json-invalid",
|
|
177
|
+
`arcs-dashboard package.json missing "type" field (expected "commonjs")`,
|
|
178
|
+
"skills/arcs-dashboard/package.json",
|
|
179
|
+
`Add "type": "commonjs" to the package.json`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
addIssue(
|
|
184
|
+
"error",
|
|
185
|
+
"package-json-invalid",
|
|
186
|
+
`arcs-dashboard package.json is not valid JSON: ${err.message}`,
|
|
187
|
+
"skills/arcs-dashboard/package.json",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// --- Check 6: REMOVED. The repo bundle is the source of truth — there is no
|
|
193
|
+
// external "config root" mirror to compare against. Drift detection against
|
|
194
|
+
// ~/.config/opencode/ has been deleted; use `arcs deploy-superpowers --dry-run`
|
|
195
|
+
// if you want to preview what would change in the deployment target.
|
|
196
|
+
|
|
197
|
+
function output() {
|
|
198
|
+
const errors = issues.filter((i) => i.severity === "error").length;
|
|
199
|
+
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
200
|
+
const result = { issues, summary: { errors, warnings } };
|
|
201
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
202
|
+
process.exitCode = errors > 0 ? 1 : 0;
|
|
203
|
+
process.exit();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
output();
|