@metabase/rde 0.1.13 → 0.1.14
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 +3 -3
- package/dist/cli.mjs +318 -55
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ rde init
|
|
|
24
24
|
|
|
25
25
|
## What `rde init` does
|
|
26
26
|
|
|
27
|
-
The wizard shows its checklist first (`✓` done, `○` to do, `–` not applicable), runs the steps still to do in order, and ends with a summary of your environment. A first run on a machine that has Docker but not yet `mb` starts like this:
|
|
27
|
+
The wizard shows its checklist first (`✓` done, `○` to do, `↑` done but behind what is published, `–` not applicable), runs the steps still to do in order, and ends with a summary of your environment. A first run on a machine that has Docker but not yet `mb` starts like this:
|
|
28
28
|
|
|
29
29
|
```
|
|
30
30
|
┌ rde init
|
|
@@ -56,8 +56,8 @@ The git step reads `–` until a license with remote sync is active; it is check
|
|
|
56
56
|
| Step | What it does |
|
|
57
57
|
| --- | --- |
|
|
58
58
|
| Check this machine | Platform, Node version, git, and a reachable Docker daemon or a Java 21+ runtime |
|
|
59
|
-
| Install the Metabase CLI | `@metabase/cli` (`mb`) at the `alpha-release-65-integration-branch` dist-tag, with the package manager that installed `rde` |
|
|
60
|
-
| Install the rde and data-app skills | The data-app skills (`metabase-data-app-setup`, `-routing`, `-actions`, `-semantic-layer`) from `metabase/metabase/skills#master`, then the `rde` and `rde-data-apps` skills from the `main` branch of `metabase/agent-skills` (the rde skill hands an explicit app request to `rde-data-apps`, which hands the build to the data-app skills), for the coding agents you pick at a terminal, from the ones found on this machine (Claude Code, Codex, Cursor, Gemini CLI, OpenCode, Windsurf). When none is found, the step is skipped and says how to add the skills once an agent is installed; `rde` installs no agent. Without a terminal, or with `--yes`, every agent found gets the skills; `--agents` picks a set ahead of time (the skills are installed for them whether or not they are on the machine yet), `--skip-agents` none; `RDE_SKILL_SOURCE` installs the two rde skills from another URL or a local checkout of `metabase/agent-skills
|
|
59
|
+
| Install the Metabase CLI | `@metabase/cli` (`mb`) at the `alpha-release-65-integration-branch` dist-tag, with the package manager that installed `rde`; an installed `mb` older than what the dist-tag names now is updated. `rde status` and `rde doctor` check only what is installed and ask no registry |
|
|
60
|
+
| Install the rde and data-app skills | The data-app skills (`metabase-data-app-setup`, `-routing`, `-actions`, `-semantic-layer`, `-migrate`) from `metabase/metabase/skills#master`, then the `rde` and `rde-data-apps` skills from the `main` branch of `metabase/agent-skills` (the rde skill hands an explicit app request to `rde-data-apps`, which hands the build to the data-app skills), for the coding agents you pick at a terminal, from the ones found on this machine (Claude Code, Codex, Cursor, Gemini CLI, OpenCode, Windsurf). When none is found, the step is skipped and says how to add the skills once an agent is installed; `rde` installs no agent. Without a terminal, or with `--yes`, every agent found gets the skills; `--agents` picks a set ahead of time (the skills are installed for them whether or not they are on the machine yet), `--skip-agents` none; `RDE_SKILL_SOURCE` installs the two rde skills from another URL or a local checkout of `metabase/agent-skills`. Installed skills whose directory changed on its branch since are installed again, without a question when every detected agent already has them all |
|
|
61
61
|
| Install Metabase | **Docker** when it is available: pulls `metabase/metabase-enterprise-head:latest` (or `--image`) and creates the container. **Jar** otherwise, or with `--runtime jar`: downloads the latest enterprise release (or the tag or `.jar` path given as `--jar`) into `~/.rde/jars/` and runs it with your Java. Either way on port 23000, or the first free port above it that nothing else claims, or `--port` |
|
|
62
62
|
| Start Metabase | Starts the container or the jar and waits for `/api/health`, showing Metabase's own progress |
|
|
63
63
|
| Create the admin user | `admin@metabase.local` with a generated password that `rde credentials` shows, or your own with `--admin prompted`, see [Credentials](#credentials) |
|
package/dist/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { defineCommand, renderUsage, runMain } from "citty";
|
|
3
3
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
4
|
import { createWriteStream, promises } from "node:fs";
|
|
5
|
-
import { delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
|
+
import { delimiter, dirname, isAbsolute, join, posix, resolve, sep } from "node:path";
|
|
6
6
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
7
7
|
import { ZodError, z } from "zod";
|
|
8
8
|
import { homedir, tmpdir, userInfo } from "node:os";
|
|
@@ -71,12 +71,14 @@ var MetabaseApiError = class extends Error {
|
|
|
71
71
|
status;
|
|
72
72
|
path;
|
|
73
73
|
errors;
|
|
74
|
+
reason;
|
|
74
75
|
constructor(failure) {
|
|
75
76
|
super(`Metabase ${failure.status} on ${failure.path}: ${failure.message}`);
|
|
76
77
|
this.name = "MetabaseApiError";
|
|
77
78
|
this.status = failure.status;
|
|
78
79
|
this.path = failure.path;
|
|
79
80
|
this.errors = failure.errors;
|
|
81
|
+
this.reason = failure.message;
|
|
80
82
|
}
|
|
81
83
|
};
|
|
82
84
|
const UNAUTHORIZED_STATUS = 401;
|
|
@@ -93,10 +95,12 @@ var MetabaseInitializingError = class extends Error {
|
|
|
93
95
|
};
|
|
94
96
|
var MetabaseUnreachableError = class extends Error {
|
|
95
97
|
url;
|
|
98
|
+
reason;
|
|
96
99
|
constructor(url, reason) {
|
|
97
100
|
super(`Metabase at ${url} is unreachable: ${reason}`);
|
|
98
101
|
this.name = "MetabaseUnreachableError";
|
|
99
102
|
this.url = url;
|
|
103
|
+
this.reason = reason;
|
|
100
104
|
}
|
|
101
105
|
};
|
|
102
106
|
var DownloadError = class extends Error {
|
|
@@ -892,6 +896,11 @@ function describeCause(error) {
|
|
|
892
896
|
if (error instanceof Error && error.cause instanceof Error) return error.cause.message;
|
|
893
897
|
return error instanceof Error ? error.message : String(error);
|
|
894
898
|
}
|
|
899
|
+
function lookupFailure(error) {
|
|
900
|
+
if (error instanceof MetabaseUnreachableError) return `${error.url} is unreachable: ${error.reason}`;
|
|
901
|
+
if (error instanceof MetabaseApiError) return `HTTP ${error.status} on ${error.path}: ${error.reason}`;
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
895
904
|
function createHttp(options) {
|
|
896
905
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
897
906
|
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
@@ -911,7 +920,11 @@ function createHttp(options) {
|
|
|
911
920
|
init.body = JSON.stringify(body);
|
|
912
921
|
}
|
|
913
922
|
try {
|
|
914
|
-
|
|
923
|
+
const response = await fetchImpl(url, init);
|
|
924
|
+
return {
|
|
925
|
+
ok: response.ok,
|
|
926
|
+
body: parseBody(response.status, await response.text())
|
|
927
|
+
};
|
|
915
928
|
} catch (error) {
|
|
916
929
|
if (options.signal.aborted) throw new AbortError();
|
|
917
930
|
if (isTimeout(error)) throw new MetabaseUnreachableError(url, `no response within ${REQUEST_TIMEOUT_MS}ms`);
|
|
@@ -919,10 +932,9 @@ function createHttp(options) {
|
|
|
919
932
|
}
|
|
920
933
|
}
|
|
921
934
|
async function requestBody(method, path, body) {
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
return parsed;
|
|
935
|
+
const received = await send(method, path, body);
|
|
936
|
+
if (!received.ok) throw failureFor(path, received.body);
|
|
937
|
+
return received.body;
|
|
926
938
|
}
|
|
927
939
|
return {
|
|
928
940
|
async requestJson(method, path, request) {
|
|
@@ -3284,7 +3296,9 @@ function adminUserStep(options, deps = hostDeps$14) {
|
|
|
3284
3296
|
//#endregion
|
|
3285
3297
|
//#region src/tools/mb.ts
|
|
3286
3298
|
const MB = "mb";
|
|
3287
|
-
const
|
|
3299
|
+
const MB_PACKAGE = "@metabase/cli";
|
|
3300
|
+
const MB_DIST_TAG = "alpha-release-65-integration-branch";
|
|
3301
|
+
const MB_INSTALL_SPEC = `${MB_PACKAGE}@${MB_DIST_TAG}`;
|
|
3288
3302
|
const GLOBAL_INSTALL = {
|
|
3289
3303
|
npm: {
|
|
3290
3304
|
command: "npm",
|
|
@@ -3458,7 +3472,6 @@ const AuthProfileStatus = z.enum([
|
|
|
3458
3472
|
const AuthProfile = z.object({
|
|
3459
3473
|
profile: z.string(),
|
|
3460
3474
|
url: z.string(),
|
|
3461
|
-
authenticated: z.boolean(),
|
|
3462
3475
|
status: AuthProfileStatus,
|
|
3463
3476
|
user: ProbedUser.nullable()
|
|
3464
3477
|
}).loose();
|
|
@@ -3540,7 +3553,14 @@ function mbTool(runner) {
|
|
|
3540
3553
|
], LogoutResult);
|
|
3541
3554
|
}
|
|
3542
3555
|
async function authList() {
|
|
3543
|
-
return (await mbJson([
|
|
3556
|
+
return (await mbJson([
|
|
3557
|
+
"auth",
|
|
3558
|
+
"list",
|
|
3559
|
+
"--fields",
|
|
3560
|
+
"profile,url,status,user",
|
|
3561
|
+
"--max-bytes",
|
|
3562
|
+
"0"
|
|
3563
|
+
], AuthList)).data;
|
|
3544
3564
|
}
|
|
3545
3565
|
async function gitSyncStatus(profile) {
|
|
3546
3566
|
return mbJson([
|
|
@@ -6116,17 +6136,45 @@ function gitSyncStep(deps = hostDeps$12) {
|
|
|
6116
6136
|
};
|
|
6117
6137
|
}
|
|
6118
6138
|
//#endregion
|
|
6139
|
+
//#region src/metabase/domain/github-content.ts
|
|
6140
|
+
const GitHubContentEntry = z.object({
|
|
6141
|
+
name: z.string(),
|
|
6142
|
+
type: z.string(),
|
|
6143
|
+
sha: z.string()
|
|
6144
|
+
}).loose();
|
|
6145
|
+
const GitHubDirectory = z.array(GitHubContentEntry);
|
|
6146
|
+
//#endregion
|
|
6147
|
+
//#region src/metabase/github.ts
|
|
6148
|
+
const GITHUB_API = "https://api.github.com";
|
|
6149
|
+
const DIRECTORY_TYPE = "dir";
|
|
6150
|
+
function githubContents(signal, options = {}) {
|
|
6151
|
+
const http = createHttp({
|
|
6152
|
+
baseUrl: GITHUB_API,
|
|
6153
|
+
auth: NO_AUTH,
|
|
6154
|
+
signal,
|
|
6155
|
+
...options
|
|
6156
|
+
});
|
|
6157
|
+
return { async subdirectoryHashes(repository, ref, directory) {
|
|
6158
|
+
const path = `/repos/${repository}/contents/${directory}${ref === null ? "" : `?ref=${encodeURIComponent(ref)}`}`;
|
|
6159
|
+
const entries = await http.requestJson("GET", path, { schema: GitHubDirectory });
|
|
6160
|
+
return new Map(entries.filter((entry) => entry.type === DIRECTORY_TYPE).map((entry) => [entry.name, entry.sha]));
|
|
6161
|
+
} };
|
|
6162
|
+
}
|
|
6163
|
+
//#endregion
|
|
6119
6164
|
//#region src/tools/skills-cli.ts
|
|
6120
6165
|
const NPX = "npx";
|
|
6121
6166
|
const SKILLS_PACKAGE = "skills@latest";
|
|
6122
6167
|
const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/main";
|
|
6123
6168
|
const RDE_SOURCE_SKILL_NAMES = ["rde", "rde-data-apps"];
|
|
6124
|
-
const
|
|
6169
|
+
const DATA_APP_REPOSITORY = "metabase/metabase";
|
|
6170
|
+
const DATA_APP_BRANCH = "master";
|
|
6171
|
+
const DATA_APP_SKILLS_SOURCE = `${DATA_APP_REPOSITORY}/skills#${DATA_APP_BRANCH}`;
|
|
6125
6172
|
const DATA_APP_SKILL_NAMES = [
|
|
6126
6173
|
"metabase-data-app-setup",
|
|
6127
6174
|
"metabase-data-app-routing",
|
|
6128
6175
|
"metabase-data-app-actions",
|
|
6129
|
-
"metabase-data-app-semantic-layer"
|
|
6176
|
+
"metabase-data-app-semantic-layer",
|
|
6177
|
+
"metabase-data-app-migrate"
|
|
6130
6178
|
];
|
|
6131
6179
|
function rdeSkillSource() {
|
|
6132
6180
|
const override = readEnv(ENV_RDE_SKILL_SOURCE);
|
|
@@ -6140,7 +6188,13 @@ const InstalledSkill = z.object({
|
|
|
6140
6188
|
source: z.string().nullable()
|
|
6141
6189
|
}).loose();
|
|
6142
6190
|
const InstalledSkillList = z.array(InstalledSkill);
|
|
6143
|
-
const
|
|
6191
|
+
const LockedSkill = z.object({
|
|
6192
|
+
source: z.string().nullish(),
|
|
6193
|
+
ref: z.string().nullish(),
|
|
6194
|
+
skillPath: z.string().nullish(),
|
|
6195
|
+
skillFolderHash: z.string().nullish()
|
|
6196
|
+
}).loose();
|
|
6197
|
+
const SkillLock = z.object({ skills: z.record(z.string(), LockedSkill) }).loose();
|
|
6144
6198
|
const GLOBAL_LOCK_FILE = ".skill-lock.json";
|
|
6145
6199
|
const PROJECT_LOCK_FILE = "skills-lock.json";
|
|
6146
6200
|
function globalSkillLockPath(home, stateHome) {
|
|
@@ -6153,16 +6207,21 @@ function xdgStateHome() {
|
|
|
6153
6207
|
const value = readEnv(ENV_XDG_STATE_HOME);
|
|
6154
6208
|
return value === void 0 || value.length === 0 ? null : value;
|
|
6155
6209
|
}
|
|
6156
|
-
function
|
|
6210
|
+
function lockedSkill(path, raw, skill) {
|
|
6157
6211
|
if (raw === null) return null;
|
|
6158
6212
|
const parsed = SkillLock.safeParse(raw);
|
|
6159
6213
|
if (!parsed.success) throw new ConfigError(`${path} is not a skills lock file: ${parsed.error.message}`);
|
|
6160
|
-
return parsed.data.skills[skill]
|
|
6214
|
+
return parsed.data.skills[skill] ?? null;
|
|
6161
6215
|
}
|
|
6162
6216
|
const hostSkillFiles = {
|
|
6163
6217
|
readJson: readJsonFile,
|
|
6164
6218
|
readText: readTextFile
|
|
6165
6219
|
};
|
|
6220
|
+
const DATA_APP_SKILLS_ORIGIN = {
|
|
6221
|
+
kind: "github",
|
|
6222
|
+
repository: DATA_APP_REPOSITORY,
|
|
6223
|
+
ref: DATA_APP_BRANCH
|
|
6224
|
+
};
|
|
6166
6225
|
const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
|
|
6167
6226
|
function skillOrigin(source, cwd) {
|
|
6168
6227
|
const github = GITHUB_URL.exec(source);
|
|
@@ -6252,7 +6311,11 @@ function hostDeps$11(ctx) {
|
|
|
6252
6311
|
return {
|
|
6253
6312
|
runner: processRunner(ctx.signal),
|
|
6254
6313
|
probe: hostAgentProbe(),
|
|
6255
|
-
skillSource: rdeSkillSource()
|
|
6314
|
+
skillSource: rdeSkillSource(),
|
|
6315
|
+
stateHome: xdgStateHome(),
|
|
6316
|
+
files: hostSkillFiles,
|
|
6317
|
+
github: githubContents(ctx.signal),
|
|
6318
|
+
cwd: process.cwd()
|
|
6256
6319
|
};
|
|
6257
6320
|
}
|
|
6258
6321
|
function detectedOnly(agents) {
|
|
@@ -6287,10 +6350,91 @@ function coveredAgentNames(entry, agents) {
|
|
|
6287
6350
|
function installedLines(entries, agents) {
|
|
6288
6351
|
return entries.map((entry) => `${entry.name} (${coveredAgentNames(entry, agents).join(", ")}): ${entry.path}`).join("\n");
|
|
6289
6352
|
}
|
|
6353
|
+
function skillSets(deps) {
|
|
6354
|
+
return [{
|
|
6355
|
+
label: "data-app",
|
|
6356
|
+
source: DATA_APP_SKILLS_SOURCE,
|
|
6357
|
+
sourceNote: "",
|
|
6358
|
+
origin: DATA_APP_SKILLS_ORIGIN,
|
|
6359
|
+
skills: DATA_APP_SKILL_NAMES
|
|
6360
|
+
}, {
|
|
6361
|
+
label: "rde",
|
|
6362
|
+
source: deps.skillSource,
|
|
6363
|
+
sourceNote: deps.skillSource === "https://github.com/metabase/agent-skills/tree/main" ? "" : ` from ${deps.skillSource}`,
|
|
6364
|
+
origin: skillOrigin(deps.skillSource, deps.cwd),
|
|
6365
|
+
skills: RDE_SOURCE_SKILL_NAMES
|
|
6366
|
+
}];
|
|
6367
|
+
}
|
|
6368
|
+
function hashLookup(github) {
|
|
6369
|
+
const asked = /* @__PURE__ */ new Map();
|
|
6370
|
+
return (repository, ref, directory) => {
|
|
6371
|
+
const key = JSON.stringify([
|
|
6372
|
+
repository,
|
|
6373
|
+
ref,
|
|
6374
|
+
directory
|
|
6375
|
+
]);
|
|
6376
|
+
let hashes = asked.get(key);
|
|
6377
|
+
if (hashes === void 0) {
|
|
6378
|
+
hashes = github.subdirectoryHashes(repository, ref, directory).catch((error) => {
|
|
6379
|
+
if (lookupFailure(error) === null) throw error;
|
|
6380
|
+
return null;
|
|
6381
|
+
});
|
|
6382
|
+
asked.set(key, hashes);
|
|
6383
|
+
}
|
|
6384
|
+
return hashes;
|
|
6385
|
+
};
|
|
6386
|
+
}
|
|
6387
|
+
async function readLockedSkills(deps) {
|
|
6388
|
+
const lockPath = globalSkillLockPath(deps.probe.home, deps.stateHome);
|
|
6389
|
+
try {
|
|
6390
|
+
const lock = await deps.files.readJson(lockPath);
|
|
6391
|
+
return new Map(REQUIRED_SKILL_NAMES.map((name) => [name, lockedSkill(lockPath, lock, name)]));
|
|
6392
|
+
} catch (error) {
|
|
6393
|
+
if (error instanceof ConfigError) return null;
|
|
6394
|
+
throw error;
|
|
6395
|
+
}
|
|
6396
|
+
}
|
|
6397
|
+
function lockedCopy(locked, origin) {
|
|
6398
|
+
if (locked === null || locked.source !== origin.repository) return null;
|
|
6399
|
+
const ref = locked.ref ?? null;
|
|
6400
|
+
if (origin.ref !== null && ref !== origin.ref) return null;
|
|
6401
|
+
if (locked.skillPath === null || locked.skillPath === void 0) return null;
|
|
6402
|
+
if (locked.skillFolderHash === null || locked.skillFolderHash === void 0) return null;
|
|
6403
|
+
const folder = posix.dirname(locked.skillPath);
|
|
6404
|
+
return {
|
|
6405
|
+
ref,
|
|
6406
|
+
directory: posix.dirname(folder),
|
|
6407
|
+
folder: posix.basename(folder),
|
|
6408
|
+
hash: locked.skillFolderHash
|
|
6409
|
+
};
|
|
6410
|
+
}
|
|
6411
|
+
async function setBehind(set, check) {
|
|
6412
|
+
const { origin } = set;
|
|
6413
|
+
if (origin.kind !== "github" || check.locked === null) return true;
|
|
6414
|
+
const copies = [];
|
|
6415
|
+
for (const skill of set.skills) {
|
|
6416
|
+
const copy = lockedCopy(check.locked.get(skill) ?? null, origin);
|
|
6417
|
+
if (copy === null) return true;
|
|
6418
|
+
copies.push(copy);
|
|
6419
|
+
}
|
|
6420
|
+
if (check.updates === "installed") return false;
|
|
6421
|
+
for (const copy of copies) {
|
|
6422
|
+
const current = await check.hashes(origin.repository, copy.ref, copy.directory);
|
|
6423
|
+
if (current !== null && current.get(copy.folder) !== copy.hash) return true;
|
|
6424
|
+
}
|
|
6425
|
+
return false;
|
|
6426
|
+
}
|
|
6427
|
+
function hasSkill(installed, skill, agent) {
|
|
6428
|
+
return installed.some((entry) => entry.name === skill && skillCoversAgent(entry, agent));
|
|
6429
|
+
}
|
|
6430
|
+
function coversAll(installed, skills, agents) {
|
|
6431
|
+
return agents.every((agent) => skills.every((skill) => hasSkill(installed, skill, agent)));
|
|
6432
|
+
}
|
|
6290
6433
|
const SOURCE_NOTES = {
|
|
6291
6434
|
override: "from --agents",
|
|
6292
6435
|
detected: "detected on this machine",
|
|
6293
|
-
chosen: "as chosen"
|
|
6436
|
+
chosen: "as chosen",
|
|
6437
|
+
installed: "updating the installed copies"
|
|
6294
6438
|
};
|
|
6295
6439
|
function agentChoice(agent) {
|
|
6296
6440
|
return {
|
|
@@ -6308,7 +6452,7 @@ async function askAgents(ctx, detected) {
|
|
|
6308
6452
|
});
|
|
6309
6453
|
return detected.filter((agent) => picked.includes(agent.id));
|
|
6310
6454
|
}
|
|
6311
|
-
async function selectAgents(ctx, all, options) {
|
|
6455
|
+
async function selectAgents(ctx, all, options, installedNow) {
|
|
6312
6456
|
switch (options.kind) {
|
|
6313
6457
|
case "detected": return {
|
|
6314
6458
|
agents: detectedOnly(all),
|
|
@@ -6327,6 +6471,10 @@ async function selectAgents(ctx, all, options) {
|
|
|
6327
6471
|
agents: [],
|
|
6328
6472
|
source: "detected"
|
|
6329
6473
|
};
|
|
6474
|
+
if (coversAll(await installedNow(), REQUIRED_SKILL_NAMES, detected)) return {
|
|
6475
|
+
agents: detected,
|
|
6476
|
+
source: "installed"
|
|
6477
|
+
};
|
|
6330
6478
|
return {
|
|
6331
6479
|
agents: await askAgents(ctx, detected),
|
|
6332
6480
|
source: "chosen"
|
|
@@ -6337,11 +6485,21 @@ async function selectAgents(ctx, all, options) {
|
|
|
6337
6485
|
function skipLine(all, source) {
|
|
6338
6486
|
switch (source) {
|
|
6339
6487
|
case "override":
|
|
6340
|
-
case "chosen":
|
|
6488
|
+
case "chosen":
|
|
6489
|
+
case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$7}\` to install it later`;
|
|
6341
6490
|
case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$7} --agents <name>\` once one is installed`;
|
|
6342
6491
|
}
|
|
6343
6492
|
}
|
|
6344
|
-
function agentsSkillsStep(options, deps = hostDeps$11) {
|
|
6493
|
+
function agentsSkillsStep(options, updates, deps = hostDeps$11) {
|
|
6494
|
+
let hashes = null;
|
|
6495
|
+
async function setCheck(resolved) {
|
|
6496
|
+
hashes ??= hashLookup(resolved.github);
|
|
6497
|
+
return {
|
|
6498
|
+
updates,
|
|
6499
|
+
locked: await readLockedSkills(resolved),
|
|
6500
|
+
hashes
|
|
6501
|
+
};
|
|
6502
|
+
}
|
|
6345
6503
|
return {
|
|
6346
6504
|
id: STEP_ID$7,
|
|
6347
6505
|
title: "Install the rde and data-app skills for your coding agents",
|
|
@@ -6350,34 +6508,42 @@ function agentsSkillsStep(options, deps = hostDeps$11) {
|
|
|
6350
6508
|
const resolved = deps(ctx);
|
|
6351
6509
|
const detected = detectedOnly(await detectAgents(resolved.probe));
|
|
6352
6510
|
if (detected.length === 0) return ctx.state.steps[STEP_ID$7]?.status === "skipped" ? "not-applicable" : "todo";
|
|
6353
|
-
|
|
6511
|
+
if (missingSkills(await skillsCli(resolved.runner).list(), detected).length > 0) return "todo";
|
|
6512
|
+
const check = await setCheck(resolved);
|
|
6513
|
+
for (const set of skillSets(resolved)) if (await setBehind(set, check)) return "outdated";
|
|
6514
|
+
return "done";
|
|
6354
6515
|
},
|
|
6355
6516
|
async run(ctx) {
|
|
6356
6517
|
const resolved = deps(ctx);
|
|
6518
|
+
const skills = skillsCli(resolved.runner);
|
|
6357
6519
|
const all = await detectAgents(resolved.probe);
|
|
6358
|
-
|
|
6520
|
+
let listing = null;
|
|
6521
|
+
const installedNow = () => listing ??= skills.list();
|
|
6522
|
+
const selection = await selectAgents(ctx, all, options, installedNow);
|
|
6359
6523
|
if (selection.agents.length === 0) {
|
|
6360
6524
|
ctx.ui.info(skipLine(all, selection.source));
|
|
6361
6525
|
return { outcome: "skipped" };
|
|
6362
6526
|
}
|
|
6363
|
-
const
|
|
6527
|
+
const before = await installedNow();
|
|
6528
|
+
const check = await setCheck(resolved);
|
|
6529
|
+
const pending = [];
|
|
6530
|
+
for (const set of skillSets(resolved)) if (!coversAll(before, set.skills, selection.agents) || await setBehind(set, check)) pending.push(set);
|
|
6531
|
+
const names = displayNames(selection.agents);
|
|
6532
|
+
if (pending.length === 0) {
|
|
6533
|
+
ctx.ui.success(`The rde and data-app skills are current for ${names}`);
|
|
6534
|
+
return { outcome: "completed" };
|
|
6535
|
+
}
|
|
6364
6536
|
const agents = selection.agents.map((agent) => agent.skillsCliName);
|
|
6365
|
-
const forWhom = `for ${
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
await skills.add({
|
|
6376
|
-
source: resolved.skillSource,
|
|
6377
|
-
skills: RDE_SOURCE_SKILL_NAMES,
|
|
6378
|
-
agents,
|
|
6379
|
-
output: ctx.ui.childOutput
|
|
6380
|
-
});
|
|
6537
|
+
const forWhom = `for ${names} (${SOURCE_NOTES[selection.source]})`;
|
|
6538
|
+
for (const set of pending) {
|
|
6539
|
+
ctx.ui.info(`Installing the ${set.label} skills ${forWhom}${set.sourceNote}`);
|
|
6540
|
+
await skills.add({
|
|
6541
|
+
source: set.source,
|
|
6542
|
+
skills: set.skills,
|
|
6543
|
+
agents,
|
|
6544
|
+
output: ctx.ui.childOutput
|
|
6545
|
+
});
|
|
6546
|
+
}
|
|
6381
6547
|
const installed = await skills.list();
|
|
6382
6548
|
const missing = unlistedSkills(installed);
|
|
6383
6549
|
if (missing.length > 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show ${missing.join(", ")}`);
|
|
@@ -6555,6 +6721,24 @@ function databasesStep(options, deps = hostDeps$10) {
|
|
|
6555
6721
|
};
|
|
6556
6722
|
}
|
|
6557
6723
|
//#endregion
|
|
6724
|
+
//#region src/metabase/domain/npm-package.ts
|
|
6725
|
+
const NpmVersionManifest = z.object({ version: z.string() }).loose();
|
|
6726
|
+
//#endregion
|
|
6727
|
+
//#region src/metabase/npm-registry.ts
|
|
6728
|
+
const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
6729
|
+
function npmRegistry(signal, options = {}) {
|
|
6730
|
+
const http = createHttp({
|
|
6731
|
+
baseUrl: NPM_REGISTRY,
|
|
6732
|
+
auth: NO_AUTH,
|
|
6733
|
+
signal,
|
|
6734
|
+
...options
|
|
6735
|
+
});
|
|
6736
|
+
return { async distTagVersion(packageName, tag) {
|
|
6737
|
+
const path = `/${encodeURIComponent(packageName)}/${encodeURIComponent(tag)}`;
|
|
6738
|
+
return (await http.requestJson("GET", path, { schema: NpmVersionManifest })).version;
|
|
6739
|
+
} };
|
|
6740
|
+
}
|
|
6741
|
+
//#endregion
|
|
6558
6742
|
//#region src/steps/install-mb.ts
|
|
6559
6743
|
const STEP_ID$5 = "install-mb";
|
|
6560
6744
|
const MIN_MB_VERSION = "0.3.0";
|
|
@@ -6569,7 +6753,8 @@ function hostDeps$9(ctx) {
|
|
|
6569
6753
|
return {
|
|
6570
6754
|
runner: processRunner(ctx.signal),
|
|
6571
6755
|
installedWith: rdeInstalledWith(),
|
|
6572
|
-
locate: locateMbOnPath
|
|
6756
|
+
locate: locateMbOnPath,
|
|
6757
|
+
publishedVersion: () => npmRegistry(ctx.signal).distTagVersion(MB_PACKAGE, MB_DIST_TAG)
|
|
6573
6758
|
};
|
|
6574
6759
|
}
|
|
6575
6760
|
async function inspectMb(runner) {
|
|
@@ -6583,6 +6768,38 @@ async function inspectMb(runner) {
|
|
|
6583
6768
|
satisfies: semver.satisfies(version, MB_RANGE, { includePrerelease: true })
|
|
6584
6769
|
};
|
|
6585
6770
|
}
|
|
6771
|
+
const UNCHECKED = { kind: "unchecked" };
|
|
6772
|
+
async function publishedMb(deps) {
|
|
6773
|
+
let raw;
|
|
6774
|
+
try {
|
|
6775
|
+
raw = await deps.publishedVersion();
|
|
6776
|
+
} catch (error) {
|
|
6777
|
+
const reason = lookupFailure(error);
|
|
6778
|
+
if (reason === null) throw error;
|
|
6779
|
+
return {
|
|
6780
|
+
kind: "unknown",
|
|
6781
|
+
reason
|
|
6782
|
+
};
|
|
6783
|
+
}
|
|
6784
|
+
const version = semver.valid(raw);
|
|
6785
|
+
if (version === null) return {
|
|
6786
|
+
kind: "unknown",
|
|
6787
|
+
reason: `the registry names "${raw}" for ${MB_DIST_TAG}, which is not a version`
|
|
6788
|
+
};
|
|
6789
|
+
return {
|
|
6790
|
+
kind: "published",
|
|
6791
|
+
version
|
|
6792
|
+
};
|
|
6793
|
+
}
|
|
6794
|
+
function coreOf(version) {
|
|
6795
|
+
return `${semver.major(version)}.${semver.minor(version)}.${semver.patch(version)}`;
|
|
6796
|
+
}
|
|
6797
|
+
function updateTo(release, installed) {
|
|
6798
|
+
if (release.kind !== "published" || release.version === installed) return null;
|
|
6799
|
+
const order = semver.compare(coreOf(installed), coreOf(release.version));
|
|
6800
|
+
if (order === 0) return semver.prerelease(installed) === null ? null : release.version;
|
|
6801
|
+
return order < 0 ? release.version : null;
|
|
6802
|
+
}
|
|
6586
6803
|
function displayCommand(install) {
|
|
6587
6804
|
return [install.command, ...install.args].join(" ");
|
|
6588
6805
|
}
|
|
@@ -6681,27 +6898,66 @@ async function stillWrong(install, after, deps) {
|
|
|
6681
6898
|
`${stillWrongAdvice(diagnosis)}.`
|
|
6682
6899
|
].join(" "));
|
|
6683
6900
|
}
|
|
6684
|
-
function
|
|
6901
|
+
async function shadowedWarning(lead, deps) {
|
|
6902
|
+
const found = await inspectMbOnPath(deps.runner, deps.locate);
|
|
6903
|
+
const diagnosis = diagnoseMb(found, deps.installedWith);
|
|
6904
|
+
const advice = diagnosis.kind === "shadowed" ? ` ${shadowedHint(diagnosis)}.` : "";
|
|
6905
|
+
return `${lead} Every mb on PATH, in order: ${mbPathLines(found).join("; ")}.${advice}`;
|
|
6906
|
+
}
|
|
6907
|
+
async function updateFor(release, installed, deps) {
|
|
6908
|
+
const version = updateTo(release, installed);
|
|
6909
|
+
if (version === null) return { kind: "current" };
|
|
6910
|
+
return (await inspectMbOnPath(deps.runner, deps.locate)).some((entry) => entry.version === version) ? {
|
|
6911
|
+
kind: "shadowed",
|
|
6912
|
+
version
|
|
6913
|
+
} : {
|
|
6914
|
+
kind: "update",
|
|
6915
|
+
version
|
|
6916
|
+
};
|
|
6917
|
+
}
|
|
6918
|
+
function installMbStep(updates, deps = hostDeps$9) {
|
|
6919
|
+
let published = null;
|
|
6920
|
+
function release(resolved) {
|
|
6921
|
+
if (updates === "installed") return Promise.resolve(UNCHECKED);
|
|
6922
|
+
published ??= publishedMb(resolved);
|
|
6923
|
+
return published;
|
|
6924
|
+
}
|
|
6685
6925
|
return {
|
|
6686
6926
|
id: STEP_ID$5,
|
|
6687
6927
|
title: "Install the Metabase CLI",
|
|
6688
6928
|
async detect(ctx) {
|
|
6689
|
-
const
|
|
6690
|
-
|
|
6929
|
+
const resolved = deps(ctx);
|
|
6930
|
+
const found = await inspectMb(resolved.runner);
|
|
6931
|
+
if (!found.installed || !found.satisfies) return "todo";
|
|
6932
|
+
return (await updateFor(await release(resolved), found.version, resolved)).kind === "update" ? "outdated" : "done";
|
|
6691
6933
|
},
|
|
6692
6934
|
async run(ctx) {
|
|
6693
6935
|
const resolved = deps(ctx);
|
|
6694
6936
|
const before = await inspectMb(resolved.runner);
|
|
6937
|
+
const target = await release(resolved);
|
|
6695
6938
|
if (before.installed && before.satisfies) {
|
|
6696
|
-
ctx.ui.
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6939
|
+
if (target.kind === "unknown") ctx.ui.warn(`Could not check for a newer mb: ${target.reason}`);
|
|
6940
|
+
const update = await updateFor(target, before.version, resolved);
|
|
6941
|
+
if (update.kind === "shadowed") {
|
|
6942
|
+
const lead = `mb ${before.version} runs first; ${update.version} is installed later on PATH.`;
|
|
6943
|
+
ctx.ui.warn(await shadowedWarning(lead, resolved));
|
|
6944
|
+
}
|
|
6945
|
+
if (update.kind !== "update") {
|
|
6946
|
+
ctx.ui.success(`mb ${before.version} is installed`);
|
|
6947
|
+
return { outcome: "completed" };
|
|
6948
|
+
}
|
|
6949
|
+
ctx.ui.info(`mb ${before.version} is installed; updating to ${update.version}`);
|
|
6950
|
+
} else if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
|
|
6700
6951
|
const install = mbInstallCommand(resolved.installedWith);
|
|
6701
6952
|
ctx.ui.info(`Running ${displayCommand(install)}`);
|
|
6702
6953
|
await runShown(resolved.runner, ctx.ui.childOutput, install.command, install.args);
|
|
6703
6954
|
const after = await inspectMb(resolved.runner);
|
|
6704
6955
|
if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
|
|
6956
|
+
const stillNewer = updateTo(target, after.version);
|
|
6957
|
+
if (stillNewer !== null) {
|
|
6958
|
+
const lead = `mb ${after.version} still runs first, not the ${stillNewer} just installed.`;
|
|
6959
|
+
ctx.ui.warn(await shadowedWarning(lead, resolved));
|
|
6960
|
+
}
|
|
6705
6961
|
ctx.ui.success(`mb ${after.version} is installed`);
|
|
6706
6962
|
return { outcome: "completed" };
|
|
6707
6963
|
}
|
|
@@ -7609,12 +7865,17 @@ const DEFAULT_STEP_OPTIONS = {
|
|
|
7609
7865
|
runtime: NO_RUNTIME_FLAGS,
|
|
7610
7866
|
agents: DETECT_AGENTS,
|
|
7611
7867
|
admin: DEFAULT_ADMIN,
|
|
7612
|
-
databases: SAMPLE_STANDS_IN
|
|
7868
|
+
databases: SAMPLE_STANDS_IN,
|
|
7869
|
+
updates: "published"
|
|
7870
|
+
};
|
|
7871
|
+
const LOCAL_STEP_OPTIONS = {
|
|
7872
|
+
...DEFAULT_STEP_OPTIONS,
|
|
7873
|
+
updates: "installed"
|
|
7613
7874
|
};
|
|
7614
7875
|
const STEP_BY_ID = {
|
|
7615
7876
|
preflight: () => preflightStep(),
|
|
7616
|
-
"install-mb": () => installMbStep(),
|
|
7617
|
-
"agents-skills": (options) => agentsSkillsStep(options.agents),
|
|
7877
|
+
"install-mb": (options) => installMbStep(options.updates),
|
|
7878
|
+
"agents-skills": (options) => agentsSkillsStep(options.agents, options.updates),
|
|
7618
7879
|
runtime: (options) => runtimeStep(options.runtime),
|
|
7619
7880
|
"start-metabase": () => startMetabaseStep(),
|
|
7620
7881
|
"admin-user": (options) => adminUserStep(options.admin),
|
|
@@ -7792,7 +8053,8 @@ function stepOptionsFrom(answers, run) {
|
|
|
7792
8053
|
},
|
|
7793
8054
|
agents: agentsOption(agents, run),
|
|
7794
8055
|
admin: admin === void 0 ? DEFAULT_STEP_OPTIONS.admin : { variant: admin.variant },
|
|
7795
|
-
databases: { request: databasesRequest(databases, run) }
|
|
8056
|
+
databases: { request: databasesRequest(databases, run) },
|
|
8057
|
+
updates: DEFAULT_STEP_OPTIONS.updates
|
|
7796
8058
|
};
|
|
7797
8059
|
}
|
|
7798
8060
|
function keyedAnswers(answers) {
|
|
@@ -8398,6 +8660,7 @@ var SessionCache = class {
|
|
|
8398
8660
|
const CHECKLIST_MARK = {
|
|
8399
8661
|
done: "✓",
|
|
8400
8662
|
todo: "○",
|
|
8663
|
+
outdated: "↑",
|
|
8401
8664
|
"not-applicable": "–"
|
|
8402
8665
|
};
|
|
8403
8666
|
const SPINNER_FAILURE_CODE = 1;
|
|
@@ -8638,7 +8901,7 @@ async function turnOf(entry, ctx, options) {
|
|
|
8638
8901
|
if (options.only !== void 0) return "not-selected";
|
|
8639
8902
|
if (entry.status === "done") return "done";
|
|
8640
8903
|
const status = await entry.step.detect(ctx);
|
|
8641
|
-
return status === "todo" ? "run" : status;
|
|
8904
|
+
return status === "todo" || status === "outdated" ? "run" : status;
|
|
8642
8905
|
}
|
|
8643
8906
|
function unusedMessage(step, turn, given) {
|
|
8644
8907
|
const hint = step.redoHint === void 0 ? "" : ` ${step.redoHint}`;
|
|
@@ -9286,7 +9549,7 @@ function withRef(source, ref) {
|
|
|
9286
9549
|
}
|
|
9287
9550
|
async function copyRef(entry, deps) {
|
|
9288
9551
|
const path = entry.scope === GLOBAL_SCOPE ? globalSkillLockPath(deps.probe.home, deps.skills.stateHome) : projectSkillLockPath(deps.skills.cwd);
|
|
9289
|
-
return
|
|
9552
|
+
return lockedSkill(path, await deps.skills.files.readJson(path), entry.name)?.ref ?? null;
|
|
9290
9553
|
}
|
|
9291
9554
|
async function judgeCopy(entry, origin, deps) {
|
|
9292
9555
|
const where = `${entry.scope} ${entry.path}`;
|
|
@@ -9548,7 +9811,7 @@ const doctor = defineRdeCommand({
|
|
|
9548
9811
|
signal: interruptSignal,
|
|
9549
9812
|
prompts
|
|
9550
9813
|
});
|
|
9551
|
-
if (await fixDoctor(report, initSteps(
|
|
9814
|
+
if (await fixDoctor(report, initSteps(LOCAL_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, ctx.secrets, deps);
|
|
9552
9815
|
}
|
|
9553
9816
|
print(report, args.json);
|
|
9554
9817
|
if (report.failed > 0) throw new Error(doctorSummary(report));
|
|
@@ -10050,7 +10313,7 @@ const start = defineRdeCommand({
|
|
|
10050
10313
|
const SUMMARY_STEP_ID = "summary";
|
|
10051
10314
|
function hostStatusDeps(instanceName, signal) {
|
|
10052
10315
|
return {
|
|
10053
|
-
steps: initSteps(
|
|
10316
|
+
steps: initSteps(LOCAL_STEP_OPTIONS),
|
|
10054
10317
|
runtime: hostRuntimeDeps(instanceName, signal),
|
|
10055
10318
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
|
|
10056
10319
|
runner: processRunner(signal)
|
|
@@ -10975,7 +11238,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
10975
11238
|
const main = defineCommand({
|
|
10976
11239
|
meta: {
|
|
10977
11240
|
name: "rde",
|
|
10978
|
-
version: "0.1.
|
|
11241
|
+
version: "0.1.14",
|
|
10979
11242
|
description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
|
|
10980
11243
|
},
|
|
10981
11244
|
subCommands: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metabase/rde",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Prepares a local machine for data-engineering work with Metabase and coding agents",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"e2e:down": "docker compose -f tests/e2e/docker-compose.yml down -v",
|
|
29
29
|
"e2e:databases:up": "docker compose -f tests/e2e/databases.compose.yml up -d --wait --wait-timeout 900",
|
|
30
30
|
"e2e:databases:down": "docker compose -f tests/e2e/databases.compose.yml down -v",
|
|
31
|
-
"test:e2e": "vitest run --project e2e --reporter=verbose tests/e2e/init.e2e.test.ts",
|
|
31
|
+
"test:e2e": "vitest run --project e2e --reporter=verbose tests/e2e/init.e2e.test.ts tests/e2e/mb-profiles.e2e.test.ts",
|
|
32
32
|
"test:e2e:databases": "vitest run --project e2e --reporter=verbose tests/e2e/databases.e2e.test.ts",
|
|
33
33
|
"check": "bun run typecheck && bun run lint && bun run format:check && bun run test",
|
|
34
34
|
"check:fix": "bun run lint:fix && bun run format"
|