@rizom/ops 0.2.0-alpha.11 → 0.2.0-alpha.12
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/dist/brains-ops.js +121 -116
- package/dist/content-repo.d.ts +9 -0
- package/dist/default-user-runner.d.ts +1 -1
- package/dist/index.js +121 -116
- package/dist/load-registry.d.ts +14 -0
- package/dist/onboard-user.d.ts +2 -2
- package/dist/reconcile-all.d.ts +2 -2
- package/dist/reconcile-cohort.d.ts +2 -2
- package/dist/reconcile-lib.d.ts +4 -2
- package/dist/run-subprocess.d.ts +1 -0
- package/dist/schema.d.ts +66 -0
- package/dist/user-runner.d.ts +5 -0
- package/package.json +1 -1
- package/templates/rover-pilot/.github/workflows/deploy.yml +16 -0
- package/templates/rover-pilot/deploy/scripts/resolve-deploy-handles.ts +3 -1
- package/templates/rover-pilot/deploy/scripts/sync-content-repo.ts +179 -0
- package/templates/rover-pilot/users/alice.yaml +3 -0
package/dist/load-registry.d.ts
CHANGED
|
@@ -14,6 +14,19 @@ export interface ResolvedCohort {
|
|
|
14
14
|
presetOverride?: PilotPreset;
|
|
15
15
|
aiApiKeyOverride?: string;
|
|
16
16
|
}
|
|
17
|
+
export interface ResolvedAnchorProfileSocialLink {
|
|
18
|
+
platform: "github" | "instagram" | "linkedin" | "email" | "website";
|
|
19
|
+
url: string;
|
|
20
|
+
label?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ResolvedAnchorProfile {
|
|
23
|
+
name: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
website?: string;
|
|
26
|
+
email?: string;
|
|
27
|
+
story?: string;
|
|
28
|
+
socialLinks?: ResolvedAnchorProfileSocialLink[];
|
|
29
|
+
}
|
|
17
30
|
export interface ResolvedUserIdentity {
|
|
18
31
|
handle: string;
|
|
19
32
|
cohort: string;
|
|
@@ -24,6 +37,7 @@ export interface ResolvedUserIdentity {
|
|
|
24
37
|
contentRepo: string;
|
|
25
38
|
discordEnabled: boolean;
|
|
26
39
|
effectiveAiApiKey: string;
|
|
40
|
+
anchorProfile: ResolvedAnchorProfile;
|
|
27
41
|
snapshotStatus: SnapshotStatus;
|
|
28
42
|
}
|
|
29
43
|
export interface ResolvedUser extends ResolvedUserIdentity {
|
package/dist/onboard-user.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { type UserRunner } from "./reconcile-lib";
|
|
2
|
-
export declare function onboardUser(rootDir: string, handle: string, runner?: UserRunner): Promise<void>;
|
|
1
|
+
import { type UserRunner, type ContentRepoSyncOptions } from "./reconcile-lib";
|
|
2
|
+
export declare function onboardUser(rootDir: string, handle: string, runner?: UserRunner, contentRepoOptions?: ContentRepoSyncOptions): Promise<void>;
|
package/dist/reconcile-all.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { type UserRunner } from "./reconcile-lib";
|
|
2
|
-
export declare function reconcileAll(rootDir: string, runner?: UserRunner): Promise<void>;
|
|
1
|
+
import { type UserRunner, type ContentRepoSyncOptions } from "./reconcile-lib";
|
|
2
|
+
export declare function reconcileAll(rootDir: string, runner?: UserRunner, contentRepoOptions?: ContentRepoSyncOptions): Promise<void>;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { type UserRunner } from "./reconcile-lib";
|
|
2
|
-
export declare function reconcileCohort(rootDir: string, cohortId: string, runner?: UserRunner): Promise<void>;
|
|
1
|
+
import { type UserRunner, type ContentRepoSyncOptions } from "./reconcile-lib";
|
|
2
|
+
export declare function reconcileCohort(rootDir: string, cohortId: string, runner?: UserRunner, contentRepoOptions?: ContentRepoSyncOptions): Promise<void>;
|
package/dist/reconcile-lib.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { type ContentRepoSyncOptions } from "./content-repo";
|
|
1
2
|
import { type PilotRegistry, type ResolvedUser } from "./load-registry";
|
|
2
3
|
import type { UserRunner } from "./user-runner";
|
|
3
|
-
export type {
|
|
4
|
-
export
|
|
4
|
+
export type { ContentRepoSyncOptions } from "./content-repo";
|
|
5
|
+
export type { ContentRepoFile, UserRunResult, UserRunner } from "./user-runner";
|
|
6
|
+
export declare function runUsers(rootDir: string, registry: PilotRegistry, users: ResolvedUser[], runner?: UserRunner, contentRepoOptions?: ContentRepoSyncOptions): Promise<void>;
|
|
5
7
|
export declare function findUser(rootDir: string, handle: string): Promise<{
|
|
6
8
|
registry: PilotRegistry;
|
|
7
9
|
user: ResolvedUser;
|
package/dist/run-subprocess.d.ts
CHANGED
package/dist/schema.d.ts
CHANGED
|
@@ -41,18 +41,84 @@ export declare const userSchema: z.ZodObject<{
|
|
|
41
41
|
enabled: boolean;
|
|
42
42
|
}>;
|
|
43
43
|
aiApiKeyOverride: z.ZodOptional<z.ZodString>;
|
|
44
|
+
anchorProfile: z.ZodOptional<z.ZodObject<{
|
|
45
|
+
name: z.ZodOptional<z.ZodString>;
|
|
46
|
+
description: z.ZodOptional<z.ZodString>;
|
|
47
|
+
website: z.ZodOptional<z.ZodString>;
|
|
48
|
+
email: z.ZodOptional<z.ZodString>;
|
|
49
|
+
story: z.ZodOptional<z.ZodString>;
|
|
50
|
+
socialLinks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
51
|
+
platform: z.ZodEnum<["github", "instagram", "linkedin", "email", "website"]>;
|
|
52
|
+
url: z.ZodString;
|
|
53
|
+
label: z.ZodOptional<z.ZodString>;
|
|
54
|
+
}, "strict", z.ZodTypeAny, {
|
|
55
|
+
url: string;
|
|
56
|
+
platform: "github" | "instagram" | "linkedin" | "email" | "website";
|
|
57
|
+
label?: string | undefined;
|
|
58
|
+
}, {
|
|
59
|
+
url: string;
|
|
60
|
+
platform: "github" | "instagram" | "linkedin" | "email" | "website";
|
|
61
|
+
label?: string | undefined;
|
|
62
|
+
}>, "many">>;
|
|
63
|
+
}, "strict", z.ZodTypeAny, {
|
|
64
|
+
email?: string | undefined;
|
|
65
|
+
website?: string | undefined;
|
|
66
|
+
name?: string | undefined;
|
|
67
|
+
description?: string | undefined;
|
|
68
|
+
story?: string | undefined;
|
|
69
|
+
socialLinks?: {
|
|
70
|
+
url: string;
|
|
71
|
+
platform: "github" | "instagram" | "linkedin" | "email" | "website";
|
|
72
|
+
label?: string | undefined;
|
|
73
|
+
}[] | undefined;
|
|
74
|
+
}, {
|
|
75
|
+
email?: string | undefined;
|
|
76
|
+
website?: string | undefined;
|
|
77
|
+
name?: string | undefined;
|
|
78
|
+
description?: string | undefined;
|
|
79
|
+
story?: string | undefined;
|
|
80
|
+
socialLinks?: {
|
|
81
|
+
url: string;
|
|
82
|
+
platform: "github" | "instagram" | "linkedin" | "email" | "website";
|
|
83
|
+
label?: string | undefined;
|
|
84
|
+
}[] | undefined;
|
|
85
|
+
}>>;
|
|
44
86
|
}, "strict", z.ZodTypeAny, {
|
|
45
87
|
handle: string;
|
|
46
88
|
discord: {
|
|
47
89
|
enabled: boolean;
|
|
48
90
|
};
|
|
49
91
|
aiApiKeyOverride?: string | undefined;
|
|
92
|
+
anchorProfile?: {
|
|
93
|
+
email?: string | undefined;
|
|
94
|
+
website?: string | undefined;
|
|
95
|
+
name?: string | undefined;
|
|
96
|
+
description?: string | undefined;
|
|
97
|
+
story?: string | undefined;
|
|
98
|
+
socialLinks?: {
|
|
99
|
+
url: string;
|
|
100
|
+
platform: "github" | "instagram" | "linkedin" | "email" | "website";
|
|
101
|
+
label?: string | undefined;
|
|
102
|
+
}[] | undefined;
|
|
103
|
+
} | undefined;
|
|
50
104
|
}, {
|
|
51
105
|
handle: string;
|
|
52
106
|
discord: {
|
|
53
107
|
enabled: boolean;
|
|
54
108
|
};
|
|
55
109
|
aiApiKeyOverride?: string | undefined;
|
|
110
|
+
anchorProfile?: {
|
|
111
|
+
email?: string | undefined;
|
|
112
|
+
website?: string | undefined;
|
|
113
|
+
name?: string | undefined;
|
|
114
|
+
description?: string | undefined;
|
|
115
|
+
story?: string | undefined;
|
|
116
|
+
socialLinks?: {
|
|
117
|
+
url: string;
|
|
118
|
+
platform: "github" | "instagram" | "linkedin" | "email" | "website";
|
|
119
|
+
label?: string | undefined;
|
|
120
|
+
}[] | undefined;
|
|
121
|
+
} | undefined;
|
|
56
122
|
}>;
|
|
57
123
|
export declare const cohortSchema: z.ZodEffects<z.ZodObject<{
|
|
58
124
|
members: z.ZodArray<z.ZodString, "many">;
|
package/dist/user-runner.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ResolvedUser } from "./load-registry";
|
|
2
|
+
export interface ContentRepoFile {
|
|
3
|
+
path: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
2
6
|
export interface UserRunResult {
|
|
3
7
|
brainYaml?: string;
|
|
4
8
|
envFile?: string;
|
|
9
|
+
contentRepoFiles?: ContentRepoFile[];
|
|
5
10
|
}
|
|
6
11
|
export type UserRunner = (user: ResolvedUser) => Promise<UserRunResult | void>;
|
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@ on:
|
|
|
12
12
|
paths:
|
|
13
13
|
- users/*/.env
|
|
14
14
|
- users/*/brain.yaml
|
|
15
|
+
- users/*/content/**
|
|
15
16
|
- deploy/**
|
|
16
17
|
- .github/workflows/deploy.yml
|
|
17
18
|
|
|
@@ -73,7 +74,15 @@ jobs:
|
|
|
73
74
|
- name: Install operator tooling
|
|
74
75
|
run: bun install
|
|
75
76
|
|
|
77
|
+
- name: Resolve selected user secret names
|
|
78
|
+
id: user_secret_names
|
|
79
|
+
run: |
|
|
80
|
+
HANDLE_SUFFIX="$(printf '%s' "$HANDLE" | tr '[:lower:]-' '[:upper:]_')"
|
|
81
|
+
echo "git_sync_token_secret_name=GIT_SYNC_TOKEN_${HANDLE_SUFFIX}" >> "$GITHUB_OUTPUT"
|
|
82
|
+
|
|
76
83
|
- name: Reconcile selected user config
|
|
84
|
+
env:
|
|
85
|
+
GIT_SYNC_TOKEN: ${{ secrets[steps.user_secret_names.outputs.git_sync_token_secret_name] }}
|
|
77
86
|
run: bunx brains-ops onboard "$GITHUB_WORKSPACE" "$HANDLE"
|
|
78
87
|
|
|
79
88
|
- name: Resolve generated user config
|
|
@@ -98,6 +107,12 @@ jobs:
|
|
|
98
107
|
PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
99
108
|
run: bun deploy/scripts/validate-secrets.ts
|
|
100
109
|
|
|
110
|
+
- name: Seed content repo
|
|
111
|
+
env:
|
|
112
|
+
CONTENT_REPO: ${{ steps.user_config.outputs.content_repo }}
|
|
113
|
+
GIT_SYNC_TOKEN: ${{ secrets[steps.user_config.outputs.git_sync_token_secret_name] }}
|
|
114
|
+
run: bun deploy/scripts/sync-content-repo.ts
|
|
115
|
+
|
|
101
116
|
- name: Log in to GHCR
|
|
102
117
|
uses: docker/login-action@v3
|
|
103
118
|
with:
|
|
@@ -213,6 +228,7 @@ jobs:
|
|
|
213
228
|
path: |
|
|
214
229
|
users/${{ matrix.handle }}/brain.yaml
|
|
215
230
|
users/${{ matrix.handle }}/.env
|
|
231
|
+
users/${{ matrix.handle }}/content
|
|
216
232
|
|
|
217
233
|
- name: Dump remote proxy diagnostics
|
|
218
234
|
if: failure()
|
|
@@ -32,7 +32,9 @@ const handles = [
|
|
|
32
32
|
diffOutput
|
|
33
33
|
.split(/\r?\n/)
|
|
34
34
|
.map((path) => {
|
|
35
|
-
const match = path.match(
|
|
35
|
+
const match = path.match(
|
|
36
|
+
/^users\/([^/]+)\/(?:\.env|brain\.yaml|content\/.*)$/,
|
|
37
|
+
);
|
|
36
38
|
return match?.[1] ?? null;
|
|
37
39
|
})
|
|
38
40
|
.filter((handle): handle is string => handle !== null)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { cp, mkdtemp, mkdir, readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { readJsonResponse, requireEnv } from "./helpers";
|
|
7
|
+
|
|
8
|
+
const handle = requireEnv("HANDLE");
|
|
9
|
+
const contentRepo = requireEnv("CONTENT_REPO");
|
|
10
|
+
const token = requireEnv("GIT_SYNC_TOKEN");
|
|
11
|
+
const sourceDir = join("users", handle, "content");
|
|
12
|
+
|
|
13
|
+
if (!existsSync(sourceDir)) {
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { owner, repo } = parseRepoSlug(contentRepo);
|
|
18
|
+
await ensureGitHubRepo({ owner, repo, token });
|
|
19
|
+
|
|
20
|
+
const tempRoot = await mkdtemp(join(tmpdir(), "brains-ops-content-"));
|
|
21
|
+
const checkoutDir = join(tempRoot, "repo");
|
|
22
|
+
const remoteUrl = buildAuthenticatedRemoteUrl(owner, repo, token);
|
|
23
|
+
|
|
24
|
+
runGit(["clone", remoteUrl, checkoutDir]);
|
|
25
|
+
runGit(["-C", checkoutDir, "checkout", "-B", "main"]);
|
|
26
|
+
|
|
27
|
+
const copiedFiles = await copyMissingFiles(sourceDir, checkoutDir);
|
|
28
|
+
if (copiedFiles === 0) {
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
runGit(["-C", checkoutDir, "config", "user.name", "brains-ops[bot]"]);
|
|
33
|
+
runGit([
|
|
34
|
+
"-C",
|
|
35
|
+
checkoutDir,
|
|
36
|
+
"config",
|
|
37
|
+
"user.email",
|
|
38
|
+
"41898282+github-actions[bot]@users.noreply.github.com",
|
|
39
|
+
]);
|
|
40
|
+
runGit(["-C", checkoutDir, "add", "."]);
|
|
41
|
+
|
|
42
|
+
if (hasNoStagedChanges(checkoutDir)) {
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
runGit([
|
|
47
|
+
"-C",
|
|
48
|
+
checkoutDir,
|
|
49
|
+
"commit",
|
|
50
|
+
"-m",
|
|
51
|
+
`chore(content): seed ${handle} anchor profile`,
|
|
52
|
+
]);
|
|
53
|
+
runGit(["-C", checkoutDir, "push", "origin", "HEAD:main"]);
|
|
54
|
+
|
|
55
|
+
const STALE_ANCHOR_PROFILE_MARKERS = [
|
|
56
|
+
"name: Your Name Here",
|
|
57
|
+
"Delete this and write your own",
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
interface EnsureGitHubRepoOptions {
|
|
61
|
+
owner: string;
|
|
62
|
+
repo: string;
|
|
63
|
+
token: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface GitHubRepoResponse {
|
|
67
|
+
clone_url?: string;
|
|
68
|
+
private?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function ensureGitHubRepo(
|
|
72
|
+
options: EnsureGitHubRepoOptions,
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
const headers = {
|
|
75
|
+
Authorization: `Bearer ${options.token}`,
|
|
76
|
+
Accept: "application/vnd.github+json",
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
};
|
|
79
|
+
const repoUrl = `https://api.github.com/repos/${options.owner}/${options.repo}`;
|
|
80
|
+
const repoResponse = await fetch(repoUrl, { headers });
|
|
81
|
+
|
|
82
|
+
if (repoResponse.ok) {
|
|
83
|
+
await readJsonResponse(repoResponse, "GitHub repo lookup");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (repoResponse.status !== 404) {
|
|
88
|
+
const payload = await readJsonResponse(repoResponse, "GitHub repo lookup");
|
|
89
|
+
throw new Error(`GitHub repo lookup failed: ${JSON.stringify(payload)}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const createResponse = await fetch(
|
|
93
|
+
`https://api.github.com/orgs/${options.owner}/repos`,
|
|
94
|
+
{
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers,
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
name: options.repo,
|
|
99
|
+
private: true,
|
|
100
|
+
auto_init: false,
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
const payload = (await readJsonResponse(
|
|
105
|
+
createResponse,
|
|
106
|
+
"GitHub repo create",
|
|
107
|
+
)) as GitHubRepoResponse;
|
|
108
|
+
|
|
109
|
+
if (!createResponse.ok) {
|
|
110
|
+
throw new Error(`GitHub repo create failed: ${JSON.stringify(payload)}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseRepoSlug(contentRepo: string): { owner: string; repo: string } {
|
|
115
|
+
const [owner, repo] = contentRepo.split("/");
|
|
116
|
+
if (!owner || !repo) {
|
|
117
|
+
throw new Error(`Invalid CONTENT_REPO: ${contentRepo}`);
|
|
118
|
+
}
|
|
119
|
+
return { owner, repo };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildAuthenticatedRemoteUrl(
|
|
123
|
+
owner: string,
|
|
124
|
+
repo: string,
|
|
125
|
+
token: string,
|
|
126
|
+
): string {
|
|
127
|
+
return `https://x-access-token:${encodeURIComponent(token)}@github.com/${owner}/${repo}.git`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function runGit(args: string[]): void {
|
|
131
|
+
execFileSync("git", args, { stdio: "inherit" });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function hasNoStagedChanges(checkoutDir: string): boolean {
|
|
135
|
+
try {
|
|
136
|
+
execFileSync("git", ["-C", checkoutDir, "diff", "--cached", "--quiet"], {
|
|
137
|
+
stdio: "ignore",
|
|
138
|
+
});
|
|
139
|
+
return true;
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function copyMissingFiles(
|
|
146
|
+
sourceDir: string,
|
|
147
|
+
targetDir: string,
|
|
148
|
+
): Promise<number> {
|
|
149
|
+
const entries = await readdir(sourceDir, { withFileTypes: true });
|
|
150
|
+
let copiedFiles = 0;
|
|
151
|
+
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
const sourcePath = join(sourceDir, entry.name);
|
|
154
|
+
const targetPath = join(targetDir, entry.name);
|
|
155
|
+
|
|
156
|
+
if (entry.isDirectory()) {
|
|
157
|
+
await mkdir(targetPath, { recursive: true });
|
|
158
|
+
copiedFiles += await copyMissingFiles(sourcePath, targetPath);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const existing = await readFile(targetPath, "utf8").catch(() => undefined);
|
|
163
|
+
if (existing !== undefined && !isStaleAnchorProfile(existing)) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
168
|
+
await cp(sourcePath, targetPath, { force: true });
|
|
169
|
+
copiedFiles += existing === (await readFile(sourcePath, "utf8")) ? 0 : 1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return copiedFiles;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isStaleAnchorProfile(content: string): boolean {
|
|
176
|
+
return STALE_ANCHOR_PROFILE_MARKERS.some((marker) =>
|
|
177
|
+
content.includes(marker),
|
|
178
|
+
);
|
|
179
|
+
}
|