@freecodecamp/universe-cli 0.11.0 → 0.13.0

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.
Files changed (2) hide show
  1. package/dist/index.cjs +516 -161
  2. package/package.json +30 -27
package/dist/index.cjs CHANGED
@@ -9609,7 +9609,7 @@ var require_public_api = /* @__PURE__ */ __commonJSMin(((exports) => {
9609
9609
  exports.stringify = stringify;
9610
9610
  }));
9611
9611
  //#endregion
9612
- //#region src/commands/create/layer-composition/build-compose-dev-yaml.ts
9612
+ //#region src/commands/create/layer-composition/build-compose-yaml.ts
9613
9613
  var import_dist = (/* @__PURE__ */ __commonJSMin(((exports) => {
9614
9614
  var composer = require_composer();
9615
9615
  var Document = require_Document();
@@ -9656,7 +9656,7 @@ var import_dist = (/* @__PURE__ */ __commonJSMin(((exports) => {
9656
9656
  exports.visit = visit.visit;
9657
9657
  exports.visitAsync = visit.visitAsync;
9658
9658
  })))();
9659
- const buildComposeDevYaml = (framework, packageManager) => {
9659
+ const buildComposeYaml = (framework, packageManager) => {
9660
9660
  const { port } = framework;
9661
9661
  const portMapping = `${port}:${port}`;
9662
9662
  const syncEntries = framework.watchSync.map((entry) => ({
@@ -9677,6 +9677,28 @@ const buildComposeDevYaml = (framework, packageManager) => {
9677
9677
  ports: [portMapping]
9678
9678
  } } });
9679
9679
  };
9680
+ const buildDevcontainerComposeYaml = () => `services:
9681
+ devcontainer:
9682
+ build:
9683
+ context: ..
9684
+ target: dev
9685
+ command: ["sleep", "infinity"]
9686
+ volumes:
9687
+ # First mount the source
9688
+ - type: bind
9689
+ source: ..
9690
+ target: /app
9691
+ consistency: cached
9692
+ # Then mount an anonymous volume to hold node_modules. Otherwise the bind mount causes the host node_modules to be used.
9693
+ - type: volume
9694
+ target: /app/node_modules
9695
+ `;
9696
+ const buildDevcontainerJson = () => `{
9697
+ "$schema": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json",
9698
+ "dockerComposeFile": ["./docker-compose.yml"],
9699
+ "service": "devcontainer",
9700
+ "workspaceFolder": "/app"
9701
+ }`;
9680
9702
  //#endregion
9681
9703
  //#region src/output/exit-codes.ts
9682
9704
  /**
@@ -9804,7 +9826,7 @@ const composeLayerFiles = (layers, pmPreinstall) => {
9804
9826
  //#region src/commands/create/layer-composition/layer-template-renderer.ts
9805
9827
  var LayerTemplateRenderer = class {
9806
9828
  render(template, context) {
9807
- return template.replaceAll("{{name}}", context.name).replaceAll("{{port}}", String(context.port)).replaceAll("{{runtime}}", context.runtime).replaceAll("{{framework}}", context.framework);
9829
+ return template.replaceAll("{{name}}", context.name).replaceAll("{{port}}", String(context.port)).replaceAll("{{runtime}}", context.runtime).replaceAll("{{framework}}", context.framework).replaceAll("{{pmVersion}}", context.pmVersion);
9808
9830
  }
9809
9831
  };
9810
9832
  //#endregion
@@ -9864,23 +9886,24 @@ const buildDockerfileData = (runtime, framework, packageManager) => {
9864
9886
  const renderDockerfile = (data) => `FROM ${data.baseImage} AS base\nWORKDIR /app\n\nFROM base AS package-manager\n${data.pmInstall} \n\nFROM package-manager AS dev\n${data.devInstall}\n${data.devCopySource}\nCMD ${JSON.stringify(data.devCmd)}\n`;
9865
9887
  const resolveWithLayers = (input, layers, labels) => {
9866
9888
  const resolvedLayers = resolveOrderedLayers(input, layers);
9867
- const composedFiles = composeLayerFiles(resolvedLayers, input.packageManager === void 0 ? void 0 : layers["package-managers"][input.packageManager]?.preinstall);
9889
+ const pmData = input.packageManager !== void 0 ? layers["package-managers"][input.packageManager] : void 0;
9890
+ const composedFiles = composeLayerFiles(resolvedLayers, pmData?.preinstall);
9868
9891
  const renderer = new LayerTemplateRenderer();
9869
9892
  const frameworkData = layers.frameworks?.[input.framework];
9870
9893
  const context = {
9871
9894
  framework: getLabel(labels, "framework", input.framework),
9872
9895
  name: input.name,
9896
+ pmVersion: pmData?.pmVersion ?? "",
9873
9897
  port: frameworkData?.port ?? 0,
9874
9898
  runtime: getLabel(labels, "runtime", input.runtime)
9875
9899
  };
9876
9900
  const renderedFiles = Object.fromEntries(Object.entries(composedFiles).map(([filePath, content]) => [filePath, renderer.render(content, context)]));
9877
9901
  const runtimeData = layers.runtime?.[input.runtime];
9878
- if (runtimeData !== void 0 && frameworkData !== void 0 && input.packageManager !== void 0) {
9879
- const pmData = layers["package-managers"][input.packageManager];
9880
- if (pmData !== void 0) {
9881
- renderedFiles["Dockerfile"] = renderDockerfile(buildDockerfileData(runtimeData, frameworkData, pmData));
9882
- renderedFiles["docker-compose.dev.yml"] = buildComposeDevYaml(frameworkData, pmData);
9883
- }
9902
+ if (runtimeData !== void 0 && frameworkData !== void 0 && pmData !== void 0) {
9903
+ renderedFiles["Dockerfile"] = renderer.render(renderDockerfile(buildDockerfileData(runtimeData, frameworkData, pmData)), context);
9904
+ renderedFiles["compose.yaml"] = renderer.render(buildComposeYaml(frameworkData, pmData), context);
9905
+ renderedFiles[".devcontainer/docker-compose.yml"] = buildDevcontainerComposeYaml();
9906
+ renderedFiles[".devcontainer/devcontainer.json"] = buildDevcontainerJson();
9884
9907
  }
9885
9908
  return {
9886
9909
  files: renderedFiles,
@@ -9905,10 +9928,10 @@ var PackageManagerService = class {
9905
9928
  this.adapters = adapters;
9906
9929
  }
9907
9930
  async specifyDeps(options) {
9908
- const { manager, projectDirectory } = options;
9931
+ const { manager, pmVersion, projectDirectory } = options;
9909
9932
  const adapter = this.adapters[manager];
9910
9933
  if (!adapter) throw new Error(`Unknown package manager: ${manager}`);
9911
- await adapter.specifyDeps(projectDirectory);
9934
+ await adapter.specifyDeps(projectDirectory, pmVersion);
9912
9935
  }
9913
9936
  };
9914
9937
  //#endregion
@@ -9933,7 +9956,7 @@ Please check that the output format of list() has not changed, and that extractV
9933
9956
  if (devDependencies !== void 0) pinned.devDependencies = pin(devDependencies);
9934
9957
  return pinned;
9935
9958
  };
9936
- const createPackageSpecifier = (config) => ({ async specifyDeps(projectDirectory) {
9959
+ const createPackageSpecifier = (config) => ({ async run(projectDirectory) {
9937
9960
  const { lockfileName, runner, deleteBeforeFirstInstall } = config;
9938
9961
  const lockfilePath = (0, node_path.join)(projectDirectory, lockfileName);
9939
9962
  if (deleteBeforeFirstInstall) try {
@@ -9965,11 +9988,11 @@ const createPackageSpecifier = (config) => ({ async specifyDeps(projectDirectory
9965
9988
  } });
9966
9989
  //#endregion
9967
9990
  //#region src/commands/create/package-manager/docker-runner.ts
9968
- const execFileAsync$2 = (0, node_util.promisify)(node_child_process.execFile);
9991
+ const execFileAsync$3 = (0, node_util.promisify)(node_child_process.execFile);
9969
9992
  const IMAGE_TAG = "universe-runner:pm";
9970
9993
  const ensureImageBuilt = async (cwd) => {
9971
9994
  const dockerfile = (0, node_path.resolve)(cwd, "Dockerfile");
9972
- await execFileAsync$2("docker", [
9995
+ await execFileAsync$3("docker", [
9973
9996
  "build",
9974
9997
  "--file",
9975
9998
  dockerfile,
@@ -9982,7 +10005,7 @@ const ensureImageBuilt = async (cwd) => {
9982
10005
  };
9983
10006
  const runCmdForFiles = async (cwd, cmd, inputs, outputs) => {
9984
10007
  await ensureImageBuilt(cwd);
9985
- const { stdout: idRaw } = await execFileAsync$2("docker", [
10008
+ const { stdout: idRaw } = await execFileAsync$3("docker", [
9986
10009
  "create",
9987
10010
  "-w",
9988
10011
  "/app",
@@ -9991,28 +10014,28 @@ const runCmdForFiles = async (cwd, cmd, inputs, outputs) => {
9991
10014
  ], { encoding: "utf8" });
9992
10015
  const id = idRaw.trim();
9993
10016
  try {
9994
- await Promise.all(inputs.map((file) => execFileAsync$2("docker", [
10017
+ await Promise.all(inputs.map((file) => execFileAsync$3("docker", [
9995
10018
  "cp",
9996
10019
  `${cwd}/${file}`,
9997
10020
  `${id}:/app/${file}`
9998
10021
  ], { encoding: "utf8" })));
9999
- await execFileAsync$2("docker", [
10022
+ await execFileAsync$3("docker", [
10000
10023
  "start",
10001
10024
  "-a",
10002
10025
  id
10003
10026
  ], { encoding: "utf8" });
10004
- await Promise.all(outputs.map((file) => execFileAsync$2("docker", [
10027
+ await Promise.all(outputs.map((file) => execFileAsync$3("docker", [
10005
10028
  "cp",
10006
10029
  `${id}:/app/${file}`,
10007
10030
  `${cwd}/${file}`
10008
10031
  ], { encoding: "utf8" })));
10009
10032
  } finally {
10010
- await execFileAsync$2("docker", ["rm", id], { encoding: "utf8" });
10033
+ await execFileAsync$3("docker", ["rm", id], { encoding: "utf8" });
10011
10034
  }
10012
10035
  };
10013
10036
  const runCmdForStdout = async (cwd, cmd, inputs) => {
10014
10037
  await ensureImageBuilt(cwd);
10015
- const { stdout: idRaw } = await execFileAsync$2("docker", [
10038
+ const { stdout: idRaw } = await execFileAsync$3("docker", [
10016
10039
  "create",
10017
10040
  "-w",
10018
10041
  "/app",
@@ -10021,19 +10044,19 @@ const runCmdForStdout = async (cwd, cmd, inputs) => {
10021
10044
  ], { encoding: "utf8" });
10022
10045
  const id = idRaw.trim();
10023
10046
  try {
10024
- await Promise.all(inputs.map((file) => execFileAsync$2("docker", [
10047
+ await Promise.all(inputs.map((file) => execFileAsync$3("docker", [
10025
10048
  "cp",
10026
10049
  `${cwd}/${file}`,
10027
10050
  `${id}:/app/${file}`
10028
10051
  ], { encoding: "utf8" })));
10029
- const { stdout } = await execFileAsync$2("docker", [
10052
+ const { stdout } = await execFileAsync$3("docker", [
10030
10053
  "start",
10031
10054
  "-a",
10032
10055
  id
10033
10056
  ], { encoding: "utf8" });
10034
10057
  return stdout;
10035
10058
  } finally {
10036
- await execFileAsync$2("docker", ["rm", id], { encoding: "utf8" });
10059
+ await execFileAsync$3("docker", ["rm", id], { encoding: "utf8" });
10037
10060
  }
10038
10061
  };
10039
10062
  //#endregion
@@ -10067,17 +10090,17 @@ const extractVersions$1 = (listOutput) => {
10067
10090
  return versions;
10068
10091
  };
10069
10092
  var BunPackageManager = class {
10070
- impl;
10093
+ runner;
10071
10094
  constructor(runner = bunRunner) {
10072
- this.impl = createPackageSpecifier({
10095
+ this.runner = runner;
10096
+ }
10097
+ specifyDeps(projectDirectory, _pmVersion) {
10098
+ return createPackageSpecifier({
10073
10099
  deleteBeforeFirstInstall: true,
10074
10100
  extractVersions: extractVersions$1,
10075
10101
  lockfileName: LOCKFILE$1,
10076
- runner
10077
- });
10078
- }
10079
- specifyDeps(projectDirectory) {
10080
- return this.impl.specifyDeps(projectDirectory);
10102
+ runner: this.runner
10103
+ }).run(projectDirectory);
10081
10104
  }
10082
10105
  };
10083
10106
  //#endregion
@@ -10088,13 +10111,13 @@ var BunPackageManager = class {
10088
10111
  */
10089
10112
  const LOCKFILE = "pnpm-lock.yaml";
10090
10113
  const MANIFESTS = ["package.json", "pnpm-workspace.yaml"];
10091
- const defaultPnpmRunner = {
10114
+ const defaultRunnerFactory = (pmVersion) => ({
10092
10115
  async installLockfileOnly(cwd) {
10093
10116
  await runCmdForFiles(cwd, [
10094
- "pnpm",
10095
- "install",
10096
- "--lockfile-only"
10097
- ], MANIFESTS, [LOCKFILE]);
10117
+ "sh",
10118
+ "-c",
10119
+ `corepack use pnpm@${pmVersion} && pnpm install --lockfile-only`
10120
+ ], MANIFESTS, [LOCKFILE, "package.json"]);
10098
10121
  },
10099
10122
  list(cwd) {
10100
10123
  return runCmdForStdout(cwd, [
@@ -10105,7 +10128,7 @@ const defaultPnpmRunner = {
10105
10128
  "--lockfile-only"
10106
10129
  ], [...MANIFESTS, LOCKFILE]);
10107
10130
  }
10108
- };
10131
+ });
10109
10132
  const extractVersions = (listOutput) => {
10110
10133
  const root = JSON.parse(listOutput)[0] ?? {};
10111
10134
  const versions = {};
@@ -10114,17 +10137,17 @@ const extractVersions = (listOutput) => {
10114
10137
  return versions;
10115
10138
  };
10116
10139
  var PnpmPackageManager = class {
10117
- impl;
10118
- constructor(runner = defaultPnpmRunner) {
10119
- this.impl = createPackageSpecifier({
10140
+ createRunner;
10141
+ constructor(createRunner = defaultRunnerFactory) {
10142
+ this.createRunner = createRunner;
10143
+ }
10144
+ async specifyDeps(projectDirectory, pmVersion) {
10145
+ await createPackageSpecifier({
10120
10146
  deleteBeforeFirstInstall: false,
10121
10147
  extractVersions,
10122
10148
  lockfileName: LOCKFILE,
10123
- runner
10124
- });
10125
- }
10126
- specifyDeps(projectDirectory) {
10127
- return this.impl.specifyDeps(projectDirectory);
10149
+ runner: this.createRunner(pmVersion)
10150
+ }).run(projectDirectory);
10128
10151
  }
10129
10152
  };
10130
10153
  //#endregion
@@ -11372,13 +11395,13 @@ var ClackPrompt = class {
11372
11395
  };
11373
11396
  //#endregion
11374
11397
  //#region src/commands/create/io/git-repo-initialiser.ts
11375
- const execFileAsync$1 = (0, node_util.promisify)(node_child_process.execFile);
11376
- const defaultRun = async (command, args, cwd) => {
11377
- await execFileAsync$1(command, args, { cwd });
11398
+ const execFileAsync$2 = (0, node_util.promisify)(node_child_process.execFile);
11399
+ const defaultRun$1 = async (command, args, cwd) => {
11400
+ await execFileAsync$2(command, args, { cwd });
11378
11401
  };
11379
11402
  var GitRepoInitialiser = class {
11380
11403
  run;
11381
- constructor(run = defaultRun) {
11404
+ constructor(run = defaultRun$1) {
11382
11405
  this.run = run;
11383
11406
  }
11384
11407
  async initialise(projectDirectory) {
@@ -11396,6 +11419,41 @@ var GitRepoInitialiser = class {
11396
11419
  }
11397
11420
  };
11398
11421
  //#endregion
11422
+ //#region src/commands/create/io/npx-skill-installer.ts
11423
+ const execFileAsync$1 = (0, node_util.promisify)(node_child_process.execFile);
11424
+ const defaultRun = async (command, args, cwd) => {
11425
+ await execFileAsync$1(command, args, { cwd });
11426
+ };
11427
+ var NpxSkillInstaller = class {
11428
+ run;
11429
+ constructor(run = defaultRun) {
11430
+ this.run = run;
11431
+ }
11432
+ async installSkills(skills, projectDirectory) {
11433
+ const skillsByRepo = /* @__PURE__ */ new Map();
11434
+ for (const { repo, skill } of skills) {
11435
+ const existing = skillsByRepo.get(repo);
11436
+ if (existing === void 0) skillsByRepo.set(repo, [skill]);
11437
+ else existing.push(skill);
11438
+ }
11439
+ try {
11440
+ for (const [repo, repoSkills] of skillsByRepo) {
11441
+ const args = [
11442
+ "--yes",
11443
+ "skills",
11444
+ "add",
11445
+ "--yes",
11446
+ repo,
11447
+ ...repoSkills.flatMap((skill) => ["--skill", skill])
11448
+ ];
11449
+ await this.run("npx", args, projectDirectory);
11450
+ }
11451
+ } catch (error) {
11452
+ throw new ConfigError(error.message);
11453
+ }
11454
+ }
11455
+ };
11456
+ //#endregion
11399
11457
  //#region node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
11400
11458
  var _a$1;
11401
11459
  function $constructor(name, initializer, params) {
@@ -15712,6 +15770,15 @@ const silentLogger = {
15712
15770
  warn: () => {}
15713
15771
  };
15714
15772
  //#endregion
15773
+ //#region src/output/spinner.ts
15774
+ const clackSpinner = () => vt();
15775
+ const silentSpinner = () => ({
15776
+ error: () => {},
15777
+ start: () => {},
15778
+ message: () => {},
15779
+ stop: () => {}
15780
+ });
15781
+ //#endregion
15715
15782
  //#region src/output/envelope.ts
15716
15783
  function buildEnvelope(command, success, data) {
15717
15784
  return {
@@ -15866,6 +15933,7 @@ const PackageManagerSchema = record(literal(["bun", "pnpm"]), strictObject({
15866
15933
  lockfile: string(),
15867
15934
  manifests: array(string()),
15868
15935
  pmInstall: string(),
15936
+ pmVersion: string().regex(/^\d+\.\d+\.\d+$/, "pmVersion must be a semver version (major.minor.patch), e.g. 1.2.3"),
15869
15937
  preinstall: string().optional()
15870
15938
  }));
15871
15939
  const ServiceSchema = record(literal([
@@ -15877,6 +15945,10 @@ const FrameworkSchema = record(string(), strictObject({
15877
15945
  devCopySource: string(),
15878
15946
  files: record(string(), string()),
15879
15947
  port: number(),
15948
+ skills: array(strictObject({
15949
+ repo: string(),
15950
+ skill: string()
15951
+ })).optional(),
15880
15952
  watchSync: array(strictObject({
15881
15953
  path: string(),
15882
15954
  target: string()
@@ -15892,19 +15964,23 @@ const LabelsSchema = record(union([
15892
15964
  literal("service")
15893
15965
  ]), record(string(), string()));
15894
15966
  //#endregion
15967
+ //#region src/commands/create/layer-composition/assets.json
15968
+ var templateVersion = "0.4.1";
15969
+ var templateUrl = "https://github.com/freeCodeCamp-Universe/templates/releases/download/app-templates-v{{version}}/app-templates-{{version}}.tar.gz";
15970
+ //#endregion
15971
+ //#region src/commands/create/layer-composition/assets.ts
15972
+ const defaultTemplateVersion = templateVersion;
15973
+ const resolveTemplateUrl = (version) => templateUrl.replaceAll("{{version}}", version);
15974
+ //#endregion
15895
15975
  //#region src/commands/create/layer-composition/template-cache.ts
15896
- const APP_DIR$2 = "universe-cli";
15897
- const TEMPLATES_DIR = "templates";
15976
+ const APP_DIR$3 = "universe-cli";
15977
+ const TEMPLATES_DIR$1 = "templates";
15898
15978
  const cacheBase = () => {
15899
15979
  const xdg = process.env["XDG_CACHE_HOME"];
15900
15980
  if (xdg && xdg.length > 0) return xdg;
15901
15981
  return (0, node_path.join)((0, node_os.homedir)(), ".cache");
15902
15982
  };
15903
- const versionFromUrl = (url) => {
15904
- const filename = url.split("/").at(-1) ?? url;
15905
- return filename.match(/^templates-(.+)\.tar\.gz$/)?.[1] ?? filename;
15906
- };
15907
- const templateCacheDir = (url) => (0, node_path.join)(cacheBase(), APP_DIR$2, TEMPLATES_DIR, versionFromUrl(url));
15983
+ const templateCacheDir = (version) => (0, node_path.join)(cacheBase(), APP_DIR$3, TEMPLATES_DIR$1, version);
15908
15984
  //#endregion
15909
15985
  //#region src/commands/create/layer-composition/template-provider.ts
15910
15986
  const execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
@@ -15919,7 +15995,7 @@ const EXPECTED_LAYER_FILES = [
15919
15995
  const EXPECTED_ROOT_ENTRIES = ["labels.json", "layers"];
15920
15996
  const readEnv = () => ({
15921
15997
  UNIVERSE_TEMPLATES_DIR: process.env["UNIVERSE_TEMPLATES_DIR"],
15922
- UNIVERSE_TEMPLATES_URL: process.env["UNIVERSE_TEMPLATES_URL"]
15998
+ UNIVERSE_TEMPLATES_VERSION: process.env["UNIVERSE_TEMPLATES_VERSION"]
15923
15999
  });
15924
16000
  const validateEntries = (dir, expected, actual) => {
15925
16001
  const expectedSet = new Set(expected);
@@ -15996,7 +16072,7 @@ const fetchTarball = async (url, fetchImpl) => {
15996
16072
  } catch {
15997
16073
  throw new ConfigError("Templates not cached. Run `universe templates fetch` or check network.");
15998
16074
  }
15999
- if (response.status === 404) throw new ConfigError(`Template not found at ${url}. Check UNIVERSE_TEMPLATES_URL.`);
16075
+ if (response.status === 404) throw new ConfigError(`Template not found at ${url}. Check UNIVERSE_TEMPLATES_VERSION.`);
16000
16076
  if (!response.ok) throw new ConfigError(`Failed to fetch templates from ${url}: HTTP ${String(response.status)}`);
16001
16077
  const arrayBuffer = await response.arrayBuffer();
16002
16078
  return Buffer.from(arrayBuffer);
@@ -16045,24 +16121,169 @@ var RemoteTemplateProvider = class {
16045
16121
  this.cacheBaseOverride = cacheBaseOverride;
16046
16122
  this.fetchImpl = fetchImpl;
16047
16123
  }
16048
- resolveCacheDir(url) {
16049
- if (this.cacheBaseOverride !== void 0) return (0, node_path.join)(this.cacheBaseOverride(), versionFromUrl(url));
16050
- return templateCacheDir(url);
16124
+ resolveCacheDir(version) {
16125
+ if (this.cacheBaseOverride !== void 0) return (0, node_path.join)(this.cacheBaseOverride, version);
16126
+ return templateCacheDir(version);
16051
16127
  }
16052
16128
  async loadLayers(options) {
16053
- const { UNIVERSE_TEMPLATES_DIR, UNIVERSE_TEMPLATES_URL } = this.env();
16129
+ const { UNIVERSE_TEMPLATES_DIR, UNIVERSE_TEMPLATES_VERSION } = this.env();
16054
16130
  if (UNIVERSE_TEMPLATES_DIR !== void 0 && UNIVERSE_TEMPLATES_DIR.length > 0) return loadFromDir(UNIVERSE_TEMPLATES_DIR);
16055
- if (UNIVERSE_TEMPLATES_URL === void 0 || UNIVERSE_TEMPLATES_URL.length === 0) throw new ConfigError("UNIVERSE_TEMPLATES_URL is not set. Set it to the URL of a universe-templates release asset.");
16056
- const cacheDir = this.resolveCacheDir(UNIVERSE_TEMPLATES_URL);
16131
+ const version = UNIVERSE_TEMPLATES_VERSION && UNIVERSE_TEMPLATES_VERSION.length > 0 ? UNIVERSE_TEMPLATES_VERSION : defaultTemplateVersion;
16132
+ const url = resolveTemplateUrl(version);
16133
+ const cacheDir = this.resolveCacheDir(version);
16057
16134
  if (options?.forceFetch) await (0, node_fs_promises.rm)(cacheDir, {
16058
16135
  force: true,
16059
16136
  recursive: true
16060
16137
  });
16061
16138
  if (await cacheHit(cacheDir)) return loadFromDir(cacheDir);
16062
- return fetchAndCache(UNIVERSE_TEMPLATES_URL, cacheDir, this.fetchImpl);
16139
+ return fetchAndCache(url, cacheDir, this.fetchImpl);
16063
16140
  }
16064
16141
  };
16065
16142
  //#endregion
16143
+ //#region src/lib/version-utils.ts
16144
+ const DEFAULT_TTL_MS = 3600 * 1e3;
16145
+ function ttlMs() {
16146
+ const raw = process.env["UNIVERSE_UPDATE_TTL_MS"];
16147
+ if (raw !== void 0) {
16148
+ const n = Number.parseInt(raw, 10);
16149
+ if (Number.isFinite(n) && n >= 0) return n;
16150
+ }
16151
+ return DEFAULT_TTL_MS;
16152
+ }
16153
+ function isDisabled() {
16154
+ const v = process.env["UNIVERSE_NO_UPDATE_CHECK"];
16155
+ return v === "1" || v === "true";
16156
+ }
16157
+ function parseCache(raw) {
16158
+ let parsed;
16159
+ try {
16160
+ parsed = JSON.parse(raw);
16161
+ } catch {
16162
+ return null;
16163
+ }
16164
+ if (typeof parsed !== "object" || parsed === null || !("latest" in parsed) || !("lastCheck" in parsed)) return null;
16165
+ const { latest, lastCheck } = parsed;
16166
+ if (typeof latest !== "string" || typeof lastCheck !== "number") return null;
16167
+ return {
16168
+ latest,
16169
+ lastCheck
16170
+ };
16171
+ }
16172
+ function compareVersions(a, b) {
16173
+ const pa = parseVersion(a);
16174
+ const pb = parseVersion(b);
16175
+ if (pa === null || pb === null) return 0;
16176
+ for (let i = 0; i < 3; i += 1) {
16177
+ const ai = pa[i] ?? 0;
16178
+ const bi = pb[i] ?? 0;
16179
+ if (ai < bi) return -1;
16180
+ if (ai > bi) return 1;
16181
+ }
16182
+ return 0;
16183
+ }
16184
+ function parseVersion(s) {
16185
+ const parts = (s.split("-")[0] ?? "").split(".");
16186
+ if (parts.length !== 3) return null;
16187
+ const nums = parts.map((p) => Number.parseInt(p, 10));
16188
+ if (nums.some((n) => Number.isNaN(n))) return null;
16189
+ return [
16190
+ nums[0],
16191
+ nums[1],
16192
+ nums[2]
16193
+ ];
16194
+ }
16195
+ function useColor() {
16196
+ if (process.env["NO_COLOR"] && process.env["NO_COLOR"].length > 0) return false;
16197
+ return process.stderr.isTTY === true;
16198
+ }
16199
+ function paint(s, code, color) {
16200
+ if (!color) return s;
16201
+ return `\x1b[${code}m${s}\x1b[0m`;
16202
+ }
16203
+ //#endregion
16204
+ //#region src/lib/template-version-check.ts
16205
+ const APP_DIR$2 = "universe-cli";
16206
+ const TEMPLATES_DIR = "templates";
16207
+ const CACHE_FILE$1 = "template-version-check.json";
16208
+ const GITHUB_LATEST_URL = "https://api.github.com/repos/freeCodeCamp-Universe/templates/releases/latest";
16209
+ const FETCH_TIMEOUT_MS$1 = 3e3;
16210
+ const TAG_PREFIX = "app-templates-v";
16211
+ function templateCheckCachePath() {
16212
+ return (0, node_path.join)(cacheBase(), APP_DIR$2, TEMPLATES_DIR, CACHE_FILE$1);
16213
+ }
16214
+ async function readTemplateCache() {
16215
+ try {
16216
+ return parseCache(await (0, node_fs_promises.readFile)(templateCheckCachePath(), "utf-8"));
16217
+ } catch {
16218
+ return null;
16219
+ }
16220
+ }
16221
+ async function writeTemplateCache(c) {
16222
+ const path = templateCheckCachePath();
16223
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(path), {
16224
+ recursive: true,
16225
+ mode: 448
16226
+ });
16227
+ await (0, node_fs_promises.writeFile)(path, JSON.stringify(c), { mode: 420 });
16228
+ }
16229
+ async function fetchLatestTemplateVersion() {
16230
+ const ctl = new AbortController();
16231
+ const timer = setTimeout(() => ctl.abort(), FETCH_TIMEOUT_MS$1);
16232
+ try {
16233
+ const res = await fetch(GITHUB_LATEST_URL, {
16234
+ signal: ctl.signal,
16235
+ headers: { accept: "application/vnd.github+json" }
16236
+ });
16237
+ if (!res.ok) return null;
16238
+ const body = await res.json();
16239
+ if (typeof body.tag_name !== "string") return null;
16240
+ if (!body.tag_name.startsWith(TAG_PREFIX)) return null;
16241
+ return body.tag_name.slice(15);
16242
+ } catch {
16243
+ return null;
16244
+ } finally {
16245
+ clearTimeout(timer);
16246
+ }
16247
+ }
16248
+ async function checkTemplateVersion(currentVersion, now = Date.now()) {
16249
+ if (isDisabled()) return null;
16250
+ const cache = await readTemplateCache();
16251
+ if (cache !== null && now - cache.lastCheck < ttlMs()) {
16252
+ if (compareVersions(currentVersion, cache.latest) < 0) return {
16253
+ current: currentVersion,
16254
+ latest: cache.latest
16255
+ };
16256
+ return null;
16257
+ }
16258
+ const latest = await fetchLatestTemplateVersion();
16259
+ if (latest === null) return null;
16260
+ try {
16261
+ await writeTemplateCache({
16262
+ latest,
16263
+ lastCheck: now
16264
+ });
16265
+ } catch {}
16266
+ if (compareVersions(currentVersion, latest) < 0) return {
16267
+ current: currentVersion,
16268
+ latest
16269
+ };
16270
+ return null;
16271
+ }
16272
+ function formatTemplateNotice(n, color = useColor()) {
16273
+ const dim = (s) => paint(s, "2", color);
16274
+ const yellow = (s) => paint(s, "33", color);
16275
+ const cyan = (s) => paint(s, "36", color);
16276
+ const bar = dim("│");
16277
+ return [
16278
+ "",
16279
+ bar,
16280
+ `${yellow("▲")} Newer templates available: ${dim(n.current)} → ${cyan(n.latest)}`,
16281
+ `${bar} Set ${cyan(`UNIVERSE_TEMPLATES_VERSION=${n.latest}`)} to use them.`,
16282
+ dim("└"),
16283
+ ""
16284
+ ].join("\n");
16285
+ }
16286
+ //#endregion
16066
16287
  //#region src/commands/create/index.ts
16067
16288
  const defaultFilesystemWriter = new LocalProjectWriter();
16068
16289
  const create$1 = async (options, deps = {}) => {
@@ -16076,7 +16297,19 @@ const create$1 = async (options, deps = {}) => {
16076
16297
  });
16077
16298
  const platformManifestGenerator = deps.platformManifestGenerator ?? new PlatformManifestService();
16078
16299
  const repoInitialiser = deps.repoInitialiser ?? new GitRepoInitialiser();
16300
+ const skillInstaller = deps.skillInstaller ?? new NpxSkillInstaller();
16301
+ const isTTY = process.stdin.isTTY;
16302
+ const spinner = deps.spinner ?? (options.json || !isTTY ? silentSpinner() : clackSpinner());
16079
16303
  try {
16304
+ const templatesDir = process.env["UNIVERSE_TEMPLATES_DIR"];
16305
+ if (!(templatesDir && templatesDir.length > 0)) {
16306
+ const envVersion = process.env["UNIVERSE_TEMPLATES_VERSION"];
16307
+ const effectiveVersion = envVersion && envVersion.length > 0 ? envVersion : defaultTemplateVersion;
16308
+ try {
16309
+ const notice = await checkTemplateVersion(effectiveVersion);
16310
+ if (notice) process.stderr.write(formatTemplateNotice(notice));
16311
+ } catch {}
16312
+ }
16080
16313
  const templateProvider = deps.templateProvider ?? new RemoteTemplateProvider();
16081
16314
  const { labels, registry } = await templateProvider.loadLayers({ forceFetch: options.forceFetch });
16082
16315
  const interactive = (deps.isTTY ?? Boolean(process.stdin.isTTY)) && !options.yes && !options.json;
@@ -16109,20 +16342,38 @@ const create$1 = async (options, deps = {}) => {
16109
16342
  ...pm !== void 0 ? { packageManager: pm } : {}
16110
16343
  };
16111
16344
  }
16345
+ spinner.start("Preparing your project");
16112
16346
  const validatedInput = validator.validateCreateInput(selections);
16347
+ spinner.message("Composing project layers");
16113
16348
  const resolvedLayers = await layerResolver.resolveLayers(validatedInput);
16114
16349
  const targetDirectory = `${cwd}/${validatedInput.name}`;
16115
16350
  const projectFiles = {
16116
16351
  ...resolvedLayers.files,
16117
16352
  "platform.yaml": platformManifestGenerator.generatePlatformManifest(validatedInput)
16118
16353
  };
16354
+ spinner.message("Writing project files");
16119
16355
  await filesystemWriter.writeProject(targetDirectory, projectFiles);
16120
16356
  const manager = validatedInput.packageManager;
16121
- if (manager !== void 0) await packageManager.specifyDeps({
16122
- manager,
16123
- projectDirectory: targetDirectory
16124
- });
16357
+ if (manager !== void 0) {
16358
+ spinner.message(`Pinning dependencies with ${manager}`);
16359
+ await packageManager.specifyDeps({
16360
+ manager,
16361
+ pmVersion: registry["package-managers"][manager]?.pmVersion ?? "",
16362
+ projectDirectory: targetDirectory
16363
+ });
16364
+ }
16365
+ const skills = registry.frameworks[validatedInput.framework]?.skills;
16366
+ if (skills && skills.length > 0) {
16367
+ spinner.message("Installing skills");
16368
+ try {
16369
+ await skillInstaller.installSkills(skills, targetDirectory);
16370
+ } catch (err) {
16371
+ logger.error(err instanceof Error ? err.message : String(err));
16372
+ }
16373
+ }
16374
+ spinner.message("Initialising git repository");
16125
16375
  await repoInitialiser.initialise(targetDirectory);
16376
+ spinner.stop("Project scaffolded");
16126
16377
  if (options.json) {
16127
16378
  emitJson(buildEnvelope("create", true, {
16128
16379
  path: targetDirectory,
@@ -16135,9 +16386,11 @@ const create$1 = async (options, deps = {}) => {
16135
16386
  }));
16136
16387
  return;
16137
16388
  }
16138
- if (interactive) logger.success(`Scaffolded project at ${targetDirectory}`);
16139
- else logger.info(`Scaffolded project '${validatedInput.name}' at ${targetDirectory}`);
16389
+ const startInstruction = `Project scaffolded. cd into ${validatedInput.name} and run \`docker compose up --watch\` to start the project`;
16390
+ if (interactive) logger.success(startInstruction);
16391
+ else logger.info(startInstruction);
16140
16392
  } catch (err) {
16393
+ spinner.error("Create failed");
16141
16394
  const code = err instanceof CliError ? err.exitCode : 10;
16142
16395
  const message = err instanceof Error ? err.message : String(err);
16143
16396
  outputError({
@@ -16566,6 +16819,21 @@ function suggest(target, candidates, threshold = 2) {
16566
16819
  }
16567
16820
  return bestD <= threshold ? best : null;
16568
16821
  }
16822
+ const auditRowArraySchema = array(object({
16823
+ id: number(),
16824
+ occurredAt: string(),
16825
+ actor: string(),
16826
+ action: string(),
16827
+ site: string().optional(),
16828
+ deployId: string().optional(),
16829
+ outcome: string(),
16830
+ requestId: string().optional(),
16831
+ detail: record(string(), unknown()).optional()
16832
+ }));
16833
+ const deploySummaryArraySchema = array(object({
16834
+ deployId: string(),
16835
+ actor: string().optional()
16836
+ }));
16569
16837
  const repoName = string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,99}$/, "must start with a letter or digit, then letters, digits, '.', '_' or '-' (max 100 chars)");
16570
16838
  const visibilitySchema = _enum(["public", "private"]);
16571
16839
  const repoStatusSchema = _enum([
@@ -16844,7 +17112,7 @@ function createProxyClient(cfg) {
16844
17112
  Authorization: await userBearer(),
16845
17113
  Accept: "application/json"
16846
17114
  }
16847
- });
17115
+ }, (raw) => deploySummaryArraySchema.parse(raw));
16848
17116
  },
16849
17117
  async getAlias(req) {
16850
17118
  const url = `${base}/api/site/${encodeURIComponent(req.site)}/alias/${encodeURIComponent(req.mode)}`;
@@ -16968,6 +17236,23 @@ function createProxyClient(cfg) {
16968
17236
  }
16969
17237
  }, (raw) => repoRowArraySchema.parse(raw));
16970
17238
  },
17239
+ async listAudit(req) {
17240
+ const params = new URLSearchParams();
17241
+ if (req?.site) params.set("site", req.site);
17242
+ if (req?.actor) params.set("actor", req.actor);
17243
+ if (req?.action) params.set("action", req.action);
17244
+ if (req?.since) params.set("since", req.since);
17245
+ if (req?.limit !== void 0) params.set("limit", String(req.limit));
17246
+ if (req?.offset !== void 0) params.set("offset", String(req.offset));
17247
+ const qs = params.toString();
17248
+ return call(`${base}/api/audit${qs ? `?${qs}` : ""}`, {
17249
+ method: "GET",
17250
+ headers: {
17251
+ Authorization: await userBearer(),
17252
+ Accept: "application/json"
17253
+ }
17254
+ }, (raw) => auditRowArraySchema.parse(raw));
17255
+ },
16971
17256
  async getRepoRequest(id) {
16972
17257
  return call(`${base}/api/repo/${encodeURIComponent(id)}`, {
16973
17258
  method: "GET",
@@ -17869,24 +18154,26 @@ async function readSiteFromYaml(cwd, read) {
17869
18154
  if (!r.ok) throw new ConfigError(r.error);
17870
18155
  return r.value.site;
17871
18156
  }
17872
- function formatTable$1(deploys) {
18157
+ function formatTable$2(deploys) {
17873
18158
  const header = [
17874
18159
  "DEPLOY ID",
17875
18160
  "TIMESTAMP",
17876
18161
  "SHA",
17877
- "STATE"
18162
+ "STATE",
18163
+ "ACTOR"
17878
18164
  ];
17879
18165
  const rows = deploys.map((d) => [
17880
18166
  d.deployId,
17881
18167
  d.timestamp ? d.timestamp.replace("T", " ").replace("Z", "") : "—",
17882
18168
  d.sha ?? "—",
17883
- d.state ?? "—"
18169
+ d.state ?? "—",
18170
+ d.actor ?? "—"
17884
18171
  ]);
17885
18172
  const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
17886
18173
  const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ");
17887
18174
  return [fmt(header), ...rows.map(fmt)].join("\n");
17888
18175
  }
17889
- async function ls$2(options, deps = {}) {
18176
+ async function ls$3(options, deps = {}) {
17890
18177
  const cwd = deps.cwd ?? process.cwd();
17891
18178
  const env = deps.env ?? process.env;
17892
18179
  const readYaml = deps.readPlatformYaml ?? defaultReadPlatformYaml$2;
@@ -17907,7 +18194,8 @@ async function ls$2(options, deps = {}) {
17907
18194
  getAuthToken: () => identity.token,
17908
18195
  timeoutMs: parseFetchTimeoutMs(env)
17909
18196
  });
17910
- const parsed = [...await client.siteDeploys({ site })].sort((a, b) => b.deployId.localeCompare(a.deployId)).map((d) => parseDeployId(d.deployId));
18197
+ const sorted = [...await client.siteDeploys({ site })].sort((a, b) => b.deployId.localeCompare(a.deployId));
18198
+ const parsed = sorted.map((d) => parseDeployId(d.deployId));
17911
18199
  let previewId = null;
17912
18200
  let productionId = null;
17913
18201
  if (parsed.length > 0) {
@@ -17921,9 +18209,10 @@ async function ls$2(options, deps = {}) {
17921
18209
  previewId = preview?.deployId ?? null;
17922
18210
  productionId = production?.deployId ?? null;
17923
18211
  }
17924
- const deploys = parsed.map((d) => ({
18212
+ const deploys = parsed.map((d, i) => ({
17925
18213
  ...d,
17926
- state: deployState(d.deployId, previewId, productionId)
18214
+ state: deployState(d.deployId, previewId, productionId),
18215
+ actor: sorted[i]?.actor
17927
18216
  }));
17928
18217
  if (options.json) {
17929
18218
  emitJson(buildEnvelope("ls", true, {
@@ -17941,7 +18230,7 @@ async function ls$2(options, deps = {}) {
17941
18230
  info(`(no deploys for ${site})`);
17942
18231
  return;
17943
18232
  }
17944
- success(formatTable$1(deploys));
18233
+ success(formatTable$2(deploys));
17945
18234
  } catch (err) {
17946
18235
  const { code, message } = wrapProxyError("ls", err);
17947
18236
  outputError({
@@ -18241,7 +18530,7 @@ function parseTeamsFlag(raw) {
18241
18530
  * Resolve identity + construct a proxy client. Throws CredentialError
18242
18531
  * if no identity is available — caller wraps via wrapProxyError.
18243
18532
  */
18244
- async function setupClient$1(deps) {
18533
+ async function setupClient$2(deps) {
18245
18534
  const env = deps.env ?? process.env;
18246
18535
  const resolveId = deps.resolveIdentity ?? resolveIdentity;
18247
18536
  const mkClient = deps.createProxyClient ?? createProxyClient;
@@ -18258,7 +18547,7 @@ async function setupClient$1(deps) {
18258
18547
  }
18259
18548
  //#endregion
18260
18549
  //#region src/commands/sites/ls.ts
18261
- function formatTable(rows) {
18550
+ function formatTable$1(rows) {
18262
18551
  if (rows.length === 0) return "No registered sites.";
18263
18552
  const headers = [
18264
18553
  "SLUG",
@@ -18276,13 +18565,13 @@ function formatTable(rows) {
18276
18565
  const fmt = (row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ");
18277
18566
  return [fmt(headers), ...cells.map(fmt)].join("\n");
18278
18567
  }
18279
- async function ls$1(options, deps = {}) {
18568
+ async function ls$2(options, deps = {}) {
18280
18569
  const command = "sites ls";
18281
18570
  const success = deps.logSuccess ?? ((s) => R.message(s));
18282
18571
  const error = deps.logError ?? ((s) => R.error(s));
18283
18572
  const exit = deps.exit ?? exitWithCode;
18284
18573
  try {
18285
- const { client, identitySource } = await setupClient$1(deps);
18574
+ const { client, identitySource } = await setupClient$2(deps);
18286
18575
  let rows = await client.listSites();
18287
18576
  let scope = "all";
18288
18577
  if (options.mine) {
@@ -18297,7 +18586,7 @@ async function ls$1(options, deps = {}) {
18297
18586
  sites: rows,
18298
18587
  identitySource
18299
18588
  }));
18300
- else success(formatTable(rows));
18589
+ else success(formatTable$1(rows));
18301
18590
  } catch (err) {
18302
18591
  const { code, message } = wrapProxyError(command, err);
18303
18592
  outputError({
@@ -18317,7 +18606,7 @@ async function register(options, deps = {}) {
18317
18606
  try {
18318
18607
  if (!options.slug || options.slug.trim().length === 0) throw new UsageError("slug is required (positional argument)");
18319
18608
  const teams = parseTeamsFlag(options.team);
18320
- const { client, identitySource } = await setupClient$1(deps);
18609
+ const { client, identitySource } = await setupClient$2(deps);
18321
18610
  const row = await client.registerSite({
18322
18611
  slug: options.slug,
18323
18612
  teams: teams.length > 0 ? teams : void 0
@@ -18355,7 +18644,7 @@ async function rm$1(options, deps = {}) {
18355
18644
  const exit = deps.exit ?? exitWithCode;
18356
18645
  try {
18357
18646
  if (!options.slug || options.slug.trim().length === 0) throw new UsageError("slug is required (positional argument)");
18358
- const { client, identitySource } = await setupClient$1(deps);
18647
+ const { client, identitySource } = await setupClient$2(deps);
18359
18648
  await client.deleteSite({ slug: options.slug });
18360
18649
  if (options.json) emitJson(buildEnvelope(command, true, {
18361
18650
  slug: options.slug,
@@ -18388,7 +18677,7 @@ async function update(options, deps = {}) {
18388
18677
  if (!options.slug || options.slug.trim().length === 0) throw new UsageError("slug is required (positional argument)");
18389
18678
  const teams = parseTeamsFlag(options.team);
18390
18679
  if (teams.length === 0) throw new UsageError("--team is required with at least one slug; use `sites rm` to remove a site");
18391
- const { client, identitySource } = await setupClient$1(deps);
18680
+ const { client, identitySource } = await setupClient$2(deps);
18392
18681
  const row = await client.updateSite({
18393
18682
  slug: options.slug,
18394
18683
  teams
@@ -18427,7 +18716,7 @@ const defaultRepoPrompts = {
18427
18716
  * Resolve identity + construct a proxy client. Throws CredentialError
18428
18717
  * when no identity is available — the caller wraps via wrapProxyError.
18429
18718
  */
18430
- async function setupClient(deps) {
18719
+ async function setupClient$1(deps) {
18431
18720
  const env = deps.env ?? process.env;
18432
18721
  const resolveId = deps.resolveIdentity ?? resolveIdentity;
18433
18722
  const mkClient = deps.createProxyClient ?? createProxyClient;
@@ -18447,6 +18736,26 @@ async function setupClient(deps) {
18447
18736
  * Render repo-request rows as an aligned text table. Returns `emptyMsg`
18448
18737
  * when there are no rows (the caller passes a status-specific phrase).
18449
18738
  */
18739
+ function humanizeDuration(ms) {
18740
+ const s = Math.floor(ms / 1e3);
18741
+ if (s < 60) return `${s}s`;
18742
+ const m = Math.floor(s / 60);
18743
+ if (m < 60) return `${m}m`;
18744
+ const h = Math.floor(m / 60);
18745
+ if (h < 24) return `${h}h${m % 60}m`;
18746
+ return `${Math.floor(h / 24)}d${h % 24}h`;
18747
+ }
18748
+ const RESOLVED_STATUSES = new Set([
18749
+ "active",
18750
+ "rejected",
18751
+ "failed"
18752
+ ]);
18753
+ function resolveLatency(r) {
18754
+ if (!RESOLVED_STATUSES.has(r.status)) return "";
18755
+ const ms = Date.parse(r.updatedAt) - Date.parse(r.createdAt);
18756
+ if (!Number.isFinite(ms) || ms < 0) return "";
18757
+ return humanizeDuration(ms);
18758
+ }
18450
18759
  function formatRepoTable(rows, emptyMsg = "No repo requests.") {
18451
18760
  if (rows.length === 0) return emptyMsg;
18452
18761
  const headers = [
@@ -18455,7 +18764,9 @@ function formatRepoTable(rows, emptyMsg = "No repo requests.") {
18455
18764
  "VIS",
18456
18765
  "STATUS",
18457
18766
  "REQUESTED BY",
18458
- "REQUESTED AT"
18767
+ "REQUESTED AT",
18768
+ "APPROVER",
18769
+ "LATENCY"
18459
18770
  ];
18460
18771
  const cells = rows.map((r) => [
18461
18772
  r.id,
@@ -18463,7 +18774,9 @@ function formatRepoTable(rows, emptyMsg = "No repo requests.") {
18463
18774
  r.visibility,
18464
18775
  r.status,
18465
18776
  r.requestedBy,
18466
- r.createdAt
18777
+ r.createdAt,
18778
+ r.approver ?? "",
18779
+ resolveLatency(r)
18467
18780
  ]);
18468
18781
  const widths = headers.map((h, i) => Math.max(h.length, ...cells.map((row) => row[i]?.length ?? 0)));
18469
18782
  const fmt = (row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ");
@@ -18482,7 +18795,7 @@ async function approve(options, deps = {}) {
18482
18795
  let identitySource;
18483
18796
  try {
18484
18797
  if (!options.id || options.id.trim().length === 0) throw new UsageError("request id is required (positional argument)");
18485
- const setup = await setupClient(deps);
18798
+ const setup = await setupClient$1(deps);
18486
18799
  const client = setup.client;
18487
18800
  identitySource = setup.identitySource;
18488
18801
  if (!options.json && !options.yes) {
@@ -18600,7 +18913,7 @@ async function create(options, deps = {}) {
18600
18913
  let client;
18601
18914
  const ensureClient = async () => {
18602
18915
  if (!client) {
18603
- const setup = await setupClient(deps);
18916
+ const setup = await setupClient$1(deps);
18604
18917
  client = setup.client;
18605
18918
  identitySource = setup.identitySource;
18606
18919
  }
@@ -18702,7 +19015,7 @@ async function create(options, deps = {}) {
18702
19015
  //#region src/commands/repo/ls.ts
18703
19016
  /** Closed set accepted by `--status`: the row statuses plus `all`. */
18704
19017
  const LS_STATUSES = [...repoStatusSchema.options, "all"];
18705
- async function ls(options, deps = {}) {
19018
+ async function ls$1(options, deps = {}) {
18706
19019
  const command = "repo ls";
18707
19020
  const message = deps.logMessage ?? ((s) => R.message(s));
18708
19021
  const error = deps.logError ?? ((s) => R.error(s));
@@ -18711,7 +19024,7 @@ async function ls(options, deps = {}) {
18711
19024
  try {
18712
19025
  const requestedStatus = options.all ? "all" : options.status;
18713
19026
  if (requestedStatus !== void 0 && !LS_STATUSES.includes(requestedStatus)) throw new UsageError(`invalid --status "${requestedStatus}": must be one of ${LS_STATUSES.join(", ")}`);
18714
- const setup = await setupClient(deps);
19027
+ const setup = await setupClient$1(deps);
18715
19028
  const client = setup.client;
18716
19029
  identitySource = setup.identitySource;
18717
19030
  const rows = await client.listRepoRequests({
@@ -18753,7 +19066,7 @@ async function reject(options, deps = {}) {
18753
19066
  let identitySource;
18754
19067
  try {
18755
19068
  if (!options.id || options.id.trim().length === 0) throw new UsageError("request id is required (positional argument)");
18756
- const setup = await setupClient(deps);
19069
+ const setup = await setupClient$1(deps);
18757
19070
  const client = setup.client;
18758
19071
  identitySource = setup.identitySource;
18759
19072
  if (!options.json && !options.yes) {
@@ -18806,7 +19119,7 @@ async function rm(options, deps = {}) {
18806
19119
  let identitySource;
18807
19120
  try {
18808
19121
  if (!options.id || options.id.trim().length === 0) throw new UsageError("request id is required (positional argument)");
18809
- const setup = await setupClient(deps);
19122
+ const setup = await setupClient$1(deps);
18810
19123
  const client = setup.client;
18811
19124
  identitySource = setup.identitySource;
18812
19125
  if (!options.json && !options.yes) {
@@ -18864,7 +19177,7 @@ async function status(options, deps = {}) {
18864
19177
  let identitySource;
18865
19178
  try {
18866
19179
  if (!options.id || options.id.trim().length === 0) throw new UsageError("request id is required (positional argument)");
18867
- const setup = await setupClient(deps);
19180
+ const setup = await setupClient$1(deps);
18868
19181
  const client = setup.client;
18869
19182
  identitySource = setup.identitySource;
18870
19183
  const row = await client.getRepoRequest(options.id);
@@ -18888,22 +19201,97 @@ async function status(options, deps = {}) {
18888
19201
  }
18889
19202
  }
18890
19203
  //#endregion
19204
+ //#region src/commands/audit/_shared.ts
19205
+ async function setupClient(deps) {
19206
+ const env = deps.env ?? process.env;
19207
+ const resolveId = deps.resolveIdentity ?? resolveIdentity;
19208
+ const mkClient = deps.createProxyClient ?? createProxyClient;
19209
+ const identity = await resolveId({ env });
19210
+ if (!identity) throw new CredentialError("No GitHub identity available. Run `universe login`, set $GITHUB_TOKEN, or install the gh CLI.");
19211
+ return {
19212
+ client: mkClient({
19213
+ baseUrl: env["UNIVERSE_PROXY_URL"] ?? "https://uploads.freecode.camp",
19214
+ getAuthToken: () => identity.token,
19215
+ timeoutMs: parseFetchTimeoutMs(env)
19216
+ }),
19217
+ identitySource: identity.source
19218
+ };
19219
+ }
19220
+ //#endregion
19221
+ //#region src/commands/audit/ls.ts
19222
+ function formatTable(rows) {
19223
+ if (rows.length === 0) return "No audit events.";
19224
+ const headers = [
19225
+ "OCCURRED AT",
19226
+ "ACTOR",
19227
+ "ACTION",
19228
+ "TARGET",
19229
+ "OUTCOME"
19230
+ ];
19231
+ const cells = rows.map((r) => [
19232
+ r.occurredAt,
19233
+ r.actor,
19234
+ r.action,
19235
+ targetOf(r),
19236
+ r.outcome
19237
+ ]);
19238
+ const widths = headers.map((h, i) => Math.max(h.length, ...cells.map((row) => row[i]?.length ?? 0)));
19239
+ const fmt = (row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ");
19240
+ return [fmt(headers), ...cells.map(fmt)].join("\n");
19241
+ }
19242
+ function targetFromDetail(r) {
19243
+ const name = r.detail?.["name"];
19244
+ return typeof name === "string" ? name : "";
19245
+ }
19246
+ function targetOf(r) {
19247
+ if (r.site && r.deployId) return `${r.site}/${r.deployId}`;
19248
+ return r.site || r.deployId || targetFromDetail(r);
19249
+ }
19250
+ async function ls(options, deps = {}) {
19251
+ const command = "audit ls";
19252
+ const message = deps.logMessage ?? ((s) => R.message(s));
19253
+ const error = deps.logError ?? ((s) => R.error(s));
19254
+ const exit = deps.exit ?? exitWithCode;
19255
+ let identitySource;
19256
+ try {
19257
+ if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 0)) throw new UsageError("--limit must be a non-negative integer");
19258
+ const setup = await setupClient(deps);
19259
+ identitySource = setup.identitySource;
19260
+ const rows = await setup.client.listAudit({
19261
+ site: options.site,
19262
+ actor: options.actor,
19263
+ action: options.action,
19264
+ since: options.since,
19265
+ limit: options.limit
19266
+ });
19267
+ if (options.json) emitJson(buildEnvelope(command, true, {
19268
+ count: rows.length,
19269
+ events: rows,
19270
+ identitySource
19271
+ }));
19272
+ else message(formatTable(rows));
19273
+ } catch (err) {
19274
+ const { code, message: msg, kind, requestId } = wrapProxyError(command, err);
19275
+ outputError({
19276
+ json: options.json,
19277
+ command
19278
+ }, code, msg, {
19279
+ logError: error,
19280
+ kind,
19281
+ requestId,
19282
+ extras: identitySource ? { identitySource } : void 0
19283
+ });
19284
+ exit(code);
19285
+ }
19286
+ }
19287
+ //#endregion
18891
19288
  //#region src/lib/update-notifier.ts
18892
19289
  const APP_DIR = "universe-cli";
18893
19290
  const CACHE_FILE = "update-check.json";
18894
19291
  const PKG_NAME = "@freecodecamp/universe-cli";
18895
19292
  const NPM_LATEST_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
18896
- const DEFAULT_TTL_MS = 3600 * 1e3;
18897
19293
  const FETCH_TIMEOUT_MS = 3e3;
18898
19294
  const REFRESH_ENV = "UNIVERSE_REFRESH_WORKER";
18899
- function ttlMs() {
18900
- const raw = process.env["UNIVERSE_UPDATE_TTL_MS"];
18901
- if (raw !== void 0) {
18902
- const n = Number.parseInt(raw, 10);
18903
- if (Number.isFinite(n) && n >= 0) return n;
18904
- }
18905
- return DEFAULT_TTL_MS;
18906
- }
18907
19295
  function latestUrl() {
18908
19296
  const override = process.env["UNIVERSE_UPDATE_URL"];
18909
19297
  return override && override.length > 0 ? override : NPM_LATEST_URL;
@@ -18916,25 +19304,6 @@ function configBase() {
18916
19304
  function cachePath() {
18917
19305
  return (0, node_path.join)(configBase(), APP_DIR, CACHE_FILE);
18918
19306
  }
18919
- function isDisabled() {
18920
- const v = process.env["UNIVERSE_NO_UPDATE_CHECK"];
18921
- return v === "1" || v === "true";
18922
- }
18923
- function parseCache(raw) {
18924
- let parsed;
18925
- try {
18926
- parsed = JSON.parse(raw);
18927
- } catch {
18928
- return null;
18929
- }
18930
- if (typeof parsed !== "object" || parsed === null || !("latest" in parsed) || !("lastCheck" in parsed)) return null;
18931
- const { latest, lastCheck } = parsed;
18932
- if (typeof latest !== "string" || typeof lastCheck !== "number") return null;
18933
- return {
18934
- latest,
18935
- lastCheck
18936
- };
18937
- }
18938
19307
  async function readCache() {
18939
19308
  try {
18940
19309
  return parseCache(await (0, node_fs_promises.readFile)(cachePath(), "utf-8"));
@@ -18974,29 +19343,6 @@ async function fetchLatest() {
18974
19343
  clearTimeout(timer);
18975
19344
  }
18976
19345
  }
18977
- function compareVersions(a, b) {
18978
- const pa = parseVersion(a);
18979
- const pb = parseVersion(b);
18980
- if (pa === null || pb === null) return 0;
18981
- for (let i = 0; i < 3; i += 1) {
18982
- const ai = pa[i] ?? 0;
18983
- const bi = pb[i] ?? 0;
18984
- if (ai < bi) return -1;
18985
- if (ai > bi) return 1;
18986
- }
18987
- return 0;
18988
- }
18989
- function parseVersion(s) {
18990
- const parts = (s.split("-")[0] ?? "").split(".");
18991
- if (parts.length !== 3) return null;
18992
- const nums = parts.map((p) => Number.parseInt(p, 10));
18993
- if (nums.some((n) => Number.isNaN(n))) return null;
18994
- return [
18995
- nums[0],
18996
- nums[1],
18997
- nums[2]
18998
- ];
18999
- }
19000
19346
  async function refreshIfStale(now = Date.now(), options = {}) {
19001
19347
  if (isDisabled()) return;
19002
19348
  if (!options.force) {
@@ -19042,14 +19388,6 @@ function getNoticeSync(current) {
19042
19388
  latest: cache.latest
19043
19389
  };
19044
19390
  }
19045
- function useColor() {
19046
- if (process.env["NO_COLOR"] && process.env["NO_COLOR"].length > 0) return false;
19047
- return process.stderr.isTTY === true;
19048
- }
19049
- function paint(s, code, color) {
19050
- if (!color) return s;
19051
- return `\x1b[${code}m${s}\x1b[0m`;
19052
- }
19053
19391
  function formatNotice(n, color = useColor()) {
19054
19392
  const dim = (s) => paint(s, "2", color);
19055
19393
  const yellow = (s) => paint(s, "33", color);
@@ -19081,7 +19419,7 @@ function installExitNotice(current) {
19081
19419
  }
19082
19420
  //#endregion
19083
19421
  //#region src/cli.ts
19084
- const version = "0.11.0";
19422
+ const version = "0.13.0";
19085
19423
  function handleActionError(command, json, err) {
19086
19424
  const ctx = {
19087
19425
  json,
@@ -19125,6 +19463,7 @@ async function run(argv = process.argv) {
19125
19463
  const sitesCli = namespaceGroup("sites", "Static site registry commands");
19126
19464
  const staticCli = namespaceGroup("static", "Static site deployment commands");
19127
19465
  const repoCli = namespaceGroup("repo", "Repository creation + approval queue commands");
19466
+ const auditCli = namespaceGroup("audit", "Audit trail query commands");
19128
19467
  sitesCli.command("register <slug>").description("Register a new static site (staff only)").option("--team <name>", "GitHub team slug (repeatable, or comma-separated). Defaults to staff.").action(async (slug, _opts, cmd) => {
19129
19468
  const opts = cmd.optsWithGlobals();
19130
19469
  try {
@@ -19140,7 +19479,7 @@ async function run(argv = process.argv) {
19140
19479
  sitesCli.command("ls").description("List sites in the registry").option("--mine", "Filter to sites your GitHub identity is authorized for").action(async (_opts, cmd) => {
19141
19480
  const opts = cmd.optsWithGlobals();
19142
19481
  try {
19143
- await ls$1({
19482
+ await ls$2({
19144
19483
  json: opts.json ?? false,
19145
19484
  mine: opts.mine ?? false
19146
19485
  });
@@ -19189,7 +19528,7 @@ async function run(argv = process.argv) {
19189
19528
  repoCli.command("ls").description("List repo requests (default: pending)").option("--status <status>", "pending | approved | active | rejected | failed | all").option("--mine", "Only requests you submitted").option("--all", "Show every state (shorthand for --status all)").action(async (_opts, cmd) => {
19190
19529
  const opts = cmd.optsWithGlobals();
19191
19530
  try {
19192
- await ls({
19531
+ await ls$1({
19193
19532
  json: opts.json ?? false,
19194
19533
  status: opts.status,
19195
19534
  mine: opts.mine ?? false,
@@ -19284,7 +19623,7 @@ async function run(argv = process.argv) {
19284
19623
  staticCli.command("ls").description("List recent deploys for a site").option("--site <site>", "Override site from platform.yaml").action(async (_opts, cmd) => {
19285
19624
  const opts = cmd.optsWithGlobals();
19286
19625
  try {
19287
- await ls$2({
19626
+ await ls$3({
19288
19627
  json: opts.json ?? false,
19289
19628
  site: opts.site
19290
19629
  });
@@ -19351,9 +19690,25 @@ async function run(argv = process.argv) {
19351
19690
  handleActionError("whoami", opts.json ?? false, err);
19352
19691
  }
19353
19692
  });
19693
+ auditCli.command("ls").description("List durable audit events (who did what)").option("--actor <login>", "Filter by GitHub actor").option("--action <action>", "Filter by action (e.g. repo.approve)").option("--site <slug>", "Filter by site").option("--since <rfc3339>", "Only events at or after this timestamp").option("--limit <n>", "Max rows (default 100, max 500)", (v) => Number.parseInt(v, 10)).action(async (_opts, cmd) => {
19694
+ const opts = cmd.optsWithGlobals();
19695
+ try {
19696
+ await ls({
19697
+ json: opts.json ?? false,
19698
+ actor: opts.actor,
19699
+ action: opts.action,
19700
+ site: opts.site,
19701
+ since: opts.since,
19702
+ limit: opts.limit
19703
+ });
19704
+ } catch (err) {
19705
+ handleActionError("audit ls", opts.json ?? false, err);
19706
+ }
19707
+ });
19354
19708
  cli.addCommand(staticCli);
19355
19709
  cli.addCommand(sitesCli);
19356
19710
  cli.addCommand(repoCli);
19711
+ cli.addCommand(auditCli);
19357
19712
  if (args.length === 0) {
19358
19713
  cli.outputHelp();
19359
19714
  return;
package/package.json CHANGED
@@ -1,24 +1,25 @@
1
1
  {
2
2
  "name": "@freecodecamp/universe-cli",
3
- "version": "0.11.0",
4
- "main": "dist/index.cjs",
5
- "scripts": {
6
- "test": "vitest run",
7
- "test:smoke": "UNIVERSE_E2E_REAL=1 vitest run tests/e2e/smoke-real-artemis.test.ts",
8
- "build": "tsdown",
9
- "lint": "oxlint src tests",
10
- "typecheck": "tsc --noEmit",
11
- "prepare": "husky",
12
- "dev": "tsx src/index.ts"
3
+ "version": "0.13.0",
4
+ "description": "Static site deployment CLI for the freeCodeCamp Universe platform",
5
+ "keywords": [
6
+ "cli",
7
+ "deployment",
8
+ "freecodecamp",
9
+ "static-site"
10
+ ],
11
+ "homepage": "https://github.com/freeCodeCamp-Universe/universe-cli#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/freeCodeCamp-Universe/universe-cli/issues"
13
14
  },
14
- "author": "Mrugesh Mohapatra <noreply@mrugesh.dev> (https://mrugesh.dev/)",
15
15
  "license": "BSD-3-Clause",
16
+ "author": "Mrugesh Mohapatra <noreply@mrugesh.dev> (https://mrugesh.dev/)",
16
17
  "repository": {
17
18
  "type": "git",
18
19
  "url": "git+https://github.com/freeCodeCamp-Universe/universe-cli.git"
19
20
  },
20
- "publishConfig": {
21
- "access": "public"
21
+ "bin": {
22
+ "universe": "dist/index.cjs"
22
23
  },
23
24
  "files": [
24
25
  "dist/",
@@ -26,8 +27,20 @@
26
27
  "LICENSE"
27
28
  ],
28
29
  "type": "module",
29
- "bin": {
30
- "universe": "dist/index.cjs"
30
+ "main": "dist/index.cjs",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "scripts": {
35
+ "test": "vitest run",
36
+ "test:smoke": "UNIVERSE_E2E_REAL=1 vitest run tests/e2e/smoke-real-artemis.test.ts",
37
+ "build": "tsdown",
38
+ "lint": "oxlint src tests",
39
+ "typecheck": "tsc --noEmit",
40
+ "prepare": "husky",
41
+ "dev": "tsx src/index.ts",
42
+ "fmt": "oxfmt",
43
+ "fmt:check": "oxfmt --check"
31
44
  },
32
45
  "dependencies": {
33
46
  "@clack/prompts": "^1.5.0",
@@ -38,6 +51,7 @@
38
51
  "devDependencies": {
39
52
  "@types/node": "^24.12.2",
40
53
  "husky": "^9.1.7",
54
+ "oxfmt": "^0.59.0",
41
55
  "oxlint": "^1.68.0",
42
56
  "postject": "1.0.0-alpha.6",
43
57
  "tsdown": "0.22.1",
@@ -45,19 +59,8 @@
45
59
  "typescript": "^5.9.3",
46
60
  "vitest": "^3.2.4"
47
61
  },
48
- "packageManager": "pnpm@10.33.0",
49
62
  "engines": {
50
63
  "node": ">=24.0.0"
51
64
  },
52
- "description": "Static site deployment CLI for the freeCodeCamp Universe platform",
53
- "homepage": "https://github.com/freeCodeCamp-Universe/universe-cli#readme",
54
- "bugs": {
55
- "url": "https://github.com/freeCodeCamp-Universe/universe-cli/issues"
56
- },
57
- "keywords": [
58
- "cli",
59
- "static-site",
60
- "deployment",
61
- "freecodecamp"
62
- ]
65
+ "packageManager": "pnpm@10.33.0"
63
66
  }