@octonodes/cli 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octonodes/cli",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Octonode API and plugin development CLI.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,7 +21,7 @@
21
21
  "prepack": "tsc -b"
22
22
  },
23
23
  "dependencies": {
24
- "@octonodes/sdk": "0.2.4",
24
+ "@octonodes/sdk": "0.2.5",
25
25
  "@supabase/supabase-js": "2.107.0",
26
26
  "esbuild": "0.28.1",
27
27
  "tar": "7.5.22",
@@ -12716,6 +12716,7 @@ var PLUGIN_BUNDLE_EXCLUDES = [
12716
12716
  "*.pem",
12717
12717
  "*.key"
12718
12718
  ];
12719
+ var MAX_CATALOG_MANIFESTS = 512;
12719
12720
 
12720
12721
  // packages/plugin/src/integrity.ts
12721
12722
  function collectFiles(dir, base, files) {
@@ -12875,6 +12876,79 @@ function loadPlugin(dir, source = "user") {
12875
12876
  function isPluginDir(dir) {
12876
12877
  return import_schema2.PLUGIN_DEFINITION_FILENAMES.some((name) => (0, import_node_fs4.existsSync)((0, import_node_path4.join)(dir, name)));
12877
12878
  }
12879
+ function pluginDirsIn(root) {
12880
+ if (!(0, import_node_fs4.existsSync)(root)) return [];
12881
+ const out = [];
12882
+ for (const name of (0, import_node_fs4.readdirSync)(root)) {
12883
+ if (name.startsWith(".")) continue;
12884
+ const dir = (0, import_node_path4.join)(root, name);
12885
+ try {
12886
+ if ((0, import_node_fs4.statSync)(dir).isDirectory() && isPluginDir(dir)) out.push(dir);
12887
+ } catch {
12888
+ }
12889
+ }
12890
+ return out;
12891
+ }
12892
+ function discoverPluginCatalog(opts = {}) {
12893
+ return discover(opts, true);
12894
+ }
12895
+ var catalogManifests = /* @__PURE__ */ new Map();
12896
+ function catalogManifest(dir) {
12897
+ const path = (0, import_plugin_runtime2.pluginDefinitionPath)(dir);
12898
+ const stat = (0, import_node_fs4.statSync)(path);
12899
+ const revision = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
12900
+ const cached = catalogManifests.get(path);
12901
+ if (cached?.revision === revision) return cached.manifest;
12902
+ const manifest2 = loadPluginManifest(dir);
12903
+ if (catalogManifests.size >= MAX_CATALOG_MANIFESTS) catalogManifests.delete(catalogManifests.keys().next().value);
12904
+ catalogManifests.set(path, { revision, manifest: manifest2 });
12905
+ return manifest2;
12906
+ }
12907
+ function discover(opts, metadataOnly) {
12908
+ const plugins = [];
12909
+ const errors = [];
12910
+ const seen = /* @__PURE__ */ new Set();
12911
+ const scanRoot = (root, source) => {
12912
+ for (const dir of pluginDirsIn(root)) {
12913
+ try {
12914
+ const manifest2 = metadataOnly ? catalogManifest(dir) : loadPluginManifest(dir);
12915
+ if (seen.has(manifest2.id)) continue;
12916
+ seen.add(manifest2.id);
12917
+ plugins.push({ manifest: manifest2, dir, source });
12918
+ } catch (err) {
12919
+ errors.push({ dir, message: err.message });
12920
+ }
12921
+ }
12922
+ };
12923
+ scanRoot(projectPluginRoot(opts.cwd), "project");
12924
+ try {
12925
+ const lock = readLockfile(opts.cwd);
12926
+ for (const [id, entry] of Object.entries(lock.plugins)) {
12927
+ if (seen.has(id)) continue;
12928
+ const dir = storeEntryDir(entry.pluginId ?? id, entry.version, entry.sha256, opts.storeRoot);
12929
+ if (!isPluginDir(dir)) {
12930
+ errors.push({
12931
+ dir,
12932
+ message: `plugin "${id}"@${entry.version} is locked but missing from the store \u2014 run \`octonode install\``
12933
+ });
12934
+ continue;
12935
+ }
12936
+ try {
12937
+ const manifest2 = metadataOnly ? catalogManifest(dir) : loadPluginManifest(dir);
12938
+ if (manifest2.id !== (entry.pluginId ?? id) || manifest2.version !== entry.version || !metadataOnly && hashPluginDir(dir) !== entry.sha256)
12939
+ throw new Error(`Locked plugin ${id} failed identity or integrity validation; run octonode install`);
12940
+ seen.add(id);
12941
+ plugins.push({ manifest: { ...manifest2, id }, dir, source: "store" });
12942
+ } catch (err) {
12943
+ errors.push({ dir, message: err.message });
12944
+ }
12945
+ }
12946
+ } catch (err) {
12947
+ errors.push({ dir: opts.cwd ?? process.cwd(), message: err.message });
12948
+ }
12949
+ scanRoot(opts.userRoot ?? userPluginRoot(), "user");
12950
+ return { plugins, errors };
12951
+ }
12878
12952
 
12879
12953
  // packages/plugin/src/dependencies.ts
12880
12954
  var import_node_child_process = require("node:child_process");
@@ -13254,7 +13328,7 @@ async function run(project, command, cwd, cacheRoot) {
13254
13328
  maxBuffer: 4 * 1024 * 1024
13255
13329
  });
13256
13330
  }
13257
- async function verifyProjectDependencies(project, names) {
13331
+ async function verifyProjectDependencies(project, names, versions = {}) {
13258
13332
  if (!names.length) return;
13259
13333
  const pnp = (0, import_node_path6.join)(project.root, ".pnp.cjs");
13260
13334
  const loader = (0, import_node_path6.join)(project.root, ".pnp.loader.mjs");
@@ -13266,14 +13340,37 @@ async function verifyProjectDependencies(project, names) {
13266
13340
  "--input-type=module",
13267
13341
  "-e",
13268
13342
  `import { createRequire } from 'node:module';
13269
- import { existsSync } from 'node:fs';
13343
+ import { existsSync, readFileSync } from 'node:fs';
13344
+ import { dirname, join } from 'node:path';
13270
13345
  import { fileURLToPath } from 'node:url';
13271
13346
  const require = createRequire(${JSON.stringify((0, import_node_path6.join)(project.target, "package.json"))});
13347
+ const versions = ${JSON.stringify(versions)};
13272
13348
  for (const name of ${JSON.stringify(names)}) {
13273
- try { require.resolve(name); } catch {
13349
+ let resolved;
13350
+ try { resolved = require.resolve(name); } catch {
13274
13351
  const entry = import.meta.resolve(name);
13352
+ resolved = entry.startsWith('file:') ? fileURLToPath(entry) : undefined;
13275
13353
  if (entry.startsWith('file:') && !existsSync(fileURLToPath(entry))) throw new Error('missing package entry: ' + name);
13276
13354
  }
13355
+ const identity = name.startsWith('@') ? name.split('/').slice(0, 2).join('/') : name.split('/')[0];
13356
+ if (Object.hasOwn(versions, identity)) {
13357
+ let directory = resolved ? dirname(resolved) : '';
13358
+ let matched = false;
13359
+ while (directory) {
13360
+ const file = join(directory, 'package.json');
13361
+ if (existsSync(file)) {
13362
+ const pkg = JSON.parse(readFileSync(file, 'utf8'));
13363
+ if (pkg.name === identity) {
13364
+ if (pkg.version !== versions[identity]) throw new Error('installed dependency version mismatch: ' + name);
13365
+ matched = true; break;
13366
+ }
13367
+ }
13368
+ const parent = dirname(directory);
13369
+ if (parent === directory) break;
13370
+ directory = parent;
13371
+ }
13372
+ if (!matched) throw new Error('cannot verify installed package: ' + name);
13373
+ }
13277
13374
  }`
13278
13375
  ],
13279
13376
  { cwd: project.target, env: (0, import_common.childProcessEnvironment)(), timeout: 3e4, maxBuffer: 1024 * 1024 }
@@ -13323,7 +13420,40 @@ async function reconcileDependencies(cwd, dependencies, options = {}) {
13323
13420
  throw new Error("invalid npm dependency subpath");
13324
13421
  }
13325
13422
  }
13423
+ const unique = /* @__PURE__ */ new Map();
13424
+ for (const dependency of dependencies) {
13425
+ const spec = dependency.spec.startsWith(`${dependency.name}@`) ? dependency.spec.slice(dependency.name.length + 1) : dependency.spec;
13426
+ const previous = unique.get(dependency.name);
13427
+ if (previous && previous.spec !== spec) throw new Error(`Conflicting dependency requests for ${dependency.name}`);
13428
+ unique.set(dependency.name, {
13429
+ ...dependency,
13430
+ spec,
13431
+ ...previous?.subpaths || dependency.subpaths ? { subpaths: [.../* @__PURE__ */ new Set([...previous?.subpaths ?? [], ...dependency.subpaths ?? []])] } : {}
13432
+ });
13433
+ }
13434
+ dependencies = [...unique.values()];
13326
13435
  const project = resolveDependencyProject(cwd);
13436
+ const declared = manifest(project.target);
13437
+ const existing = { ...declared.devDependencies, ...declared.optionalDependencies, ...declared.dependencies };
13438
+ if (options.preserveExisting) {
13439
+ for (const { name, spec } of dependencies)
13440
+ if (Object.hasOwn(existing, name) && existing[name] !== spec && !(existing[name].startsWith("file:") && spec.startsWith("file:") && (0, import_node_path6.resolve)(project.target, existing[name].slice(5)) === (0, import_node_path6.resolve)(project.target, spec.slice(5))))
13441
+ throw new Error(
13442
+ `Dependency conflict: ${name} is ${existing[name]}, plugin requires ${spec}; reconcile it explicitly first`
13443
+ );
13444
+ }
13445
+ const reusable = dependencies.length > 0 && !options.remove?.length && dependencies.every(
13446
+ ({ name, spec }) => existing[name] === spec && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(spec)
13447
+ );
13448
+ const reuse = reusable && await verifyProjectDependencies(
13449
+ project,
13450
+ dependencies.flatMap(({ name, subpaths }) => subpaths ? subpaths.map((path) => name + path.slice(1)) : [name]),
13451
+ Object.fromEntries(dependencies.map(({ name, spec }) => [name, spec]))
13452
+ ).then(
13453
+ () => true,
13454
+ () => false
13455
+ );
13456
+ const metadataOnly = options.metadataOnly || reuse;
13327
13457
  const packageJson = (0, import_node_path6.join)(project.target, "package.json");
13328
13458
  if (!dependencies.length && !(0, import_node_fs6.existsSync)(packageJson) && !options.commit) return;
13329
13459
  const hasLock = PACKAGE_MANAGER_LOCKS[project.manager].some((file) => (0, import_node_fs6.existsSync)((0, import_node_path6.join)(project.root, file)));
@@ -13359,13 +13489,12 @@ async function reconcileDependencies(cwd, dependencies, options = {}) {
13359
13489
  else (0, import_node_fs6.writeFileSync)(path, content);
13360
13490
  }
13361
13491
  };
13362
- if (hadInstall && !hasLock && !options.metadataOnly)
13492
+ if (hadInstall && !hasLock && !metadataOnly)
13363
13493
  throw new Error(
13364
13494
  "create a native lockfile before updating existing dependencies so failed installations can be recovered"
13365
13495
  );
13366
13496
  try {
13367
- if (options.metadataOnly && (0, import_node_fs6.existsSync)((0, import_node_path6.dirname)(journal)))
13368
- (0, import_node_fs6.writeFileSync)((0, import_node_path6.join)((0, import_node_path6.dirname)(journal), "metadata-only"), "1");
13497
+ if (metadataOnly && (0, import_node_fs6.existsSync)((0, import_node_path6.dirname)(journal))) (0, import_node_fs6.writeFileSync)((0, import_node_path6.join)((0, import_node_path6.dirname)(journal), "metadata-only"), "1");
13369
13498
  if ((0, import_node_fs6.existsSync)((0, import_node_path6.dirname)(journal)))
13370
13499
  (0, import_node_fs6.writeFileSync)(
13371
13500
  journal,
@@ -13376,7 +13505,7 @@ async function reconcileDependencies(cwd, dependencies, options = {}) {
13376
13505
  }))
13377
13506
  )
13378
13507
  );
13379
- if (options.metadataOnly) {
13508
+ if (metadataOnly) {
13380
13509
  metadataTouched = true;
13381
13510
  options.commit?.();
13382
13511
  (0, import_node_fs6.rmSync)(journal, { force: true });
@@ -13468,14 +13597,14 @@ async function reconcileDependencies(cwd, dependencies, options = {}) {
13468
13597
  }
13469
13598
  restoreBefore();
13470
13599
  let recovery = "";
13471
- if (!options.metadataOnly && hadInstall && hasLock) {
13600
+ if (!metadataOnly && hadInstall && hasLock) {
13472
13601
  try {
13473
13602
  await run(project, installCommand(project, true), project.root, options.cacheRoot);
13474
13603
  } catch {
13475
13604
  recovery = "; manifests restored, but dependency recovery failed \u2014 run a frozen install before executing workflows";
13476
13605
  }
13477
13606
  restoreBefore();
13478
- } else if (!options.metadataOnly && !hadInstall) {
13607
+ } else if (!metadataOnly && !hadInstall) {
13479
13608
  (0, import_node_fs6.rmSync)((0, import_node_path6.join)(project.root, "node_modules"), { recursive: true, force: true });
13480
13609
  }
13481
13610
  for (const [path, content] of pnpBefore) {
@@ -13780,6 +13909,19 @@ async function generatePluginPackage(cwd, selections, expectedPacker) {
13780
13909
  function pluginNpmDependencies(manifest2) {
13781
13910
  return manifest2.integration?.npmDependencies ?? (manifest2.integration?.npm ? [manifest2.integration.npm] : []);
13782
13911
  }
13912
+ function assertUniqueNpmPlugins(plugins) {
13913
+ const owners = /* @__PURE__ */ new Map();
13914
+ for (const { alias, manifest: manifest2 } of plugins) {
13915
+ const pkg = manifest2.integration?.npm?.package.trim().toLowerCase();
13916
+ if (!pkg) continue;
13917
+ const previous = owners.get(pkg);
13918
+ if (previous && previous !== alias)
13919
+ throw new Error(
13920
+ `Use existing npm integration "${previous}" for ${pkg}; duplicate plugin alias "${alias}" is not allowed`
13921
+ );
13922
+ owners.set(pkg, alias);
13923
+ }
13924
+ }
13783
13925
 
13784
13926
  // packages/plugin/src/prepared/invocation.ts
13785
13927
  var import_common3 = __toESM(require_dist2());
@@ -13828,6 +13970,10 @@ async function reconcilePluginLock(cwd, next, options = {}) {
13828
13970
  throw new Error(`Plugin identity or integrity mismatch: ${alias}`);
13829
13971
  return { alias, directory, sha256: pin.sha256, manifest: manifest2, remote: preparedPluginPin(directory, pin) };
13830
13972
  });
13973
+ assertUniqueNpmPlugins([
13974
+ ...discoverPluginCatalog({ cwd, storeRoot: options.storeRoot }).plugins.filter((item) => item.source === "project" && !loaded.some((pin) => pin.alias === item.manifest.id)).map((item) => ({ alias: item.manifest.id, manifest: item.manifest })),
13975
+ ...loaded
13976
+ ]);
13831
13977
  const libraries = loaded.filter((item) => item.manifest.library);
13832
13978
  const generated = libraries.length ? await generatePluginPackage(project.target, libraries) : void 0;
13833
13979
  const packageFile = (0, import_node_path10.join)(project.target, "package.json");
@@ -13924,6 +14070,9 @@ function installRoot(opts) {
13924
14070
  return opts.project ? { root: projectPluginRoot(opts.cwd), source: "project" } : { root: userPluginRoot(), source: "user" };
13925
14071
  }
13926
14072
  async function installNodeDeps(dir, opts = {}) {
14073
+ return installNodeDepsUnlocked(dir, opts, false);
14074
+ }
14075
+ async function installNodeDepsUnlocked(dir, opts, locked) {
13927
14076
  if (opts.noDeps) return {};
13928
14077
  const dependencies = pluginNpmDependencies(loadPluginManifest(dir));
13929
14078
  if (dependencies.length) {
@@ -13936,11 +14085,12 @@ async function installNodeDeps(dir, opts = {}) {
13936
14085
  });
13937
14086
  return {};
13938
14087
  }
13939
- await installProjectDependencies(
14088
+ await (locked ? reconcileDependencies : installProjectDependencies)(
13940
14089
  opts.cwd ?? process.cwd(),
13941
14090
  dependencies.map((npm) => ({ name: npm.package, spec: npm.spec })),
13942
14091
  {
13943
- cacheRoot: opts.cacheRoot
14092
+ cacheRoot: opts.cacheRoot,
14093
+ preserveExisting: true
13944
14094
  }
13945
14095
  );
13946
14096
  return { depsInstalled: true };
@@ -13976,6 +14126,10 @@ async function installNodeDeps(dir, opts = {}) {
13976
14126
  }
13977
14127
  }
13978
14128
  async function installPlugin(src, opts = {}) {
14129
+ if (opts.project) return withDependencyInstall(opts.cwd ?? process.cwd(), () => installPluginUnlocked(src, opts));
14130
+ return installPluginUnlocked(src, opts);
14131
+ }
14132
+ async function installPluginUnlocked(src, opts) {
13979
14133
  if (!(0, import_node_fs11.existsSync)(src) || !(0, import_node_fs11.statSync)(src).isDirectory()) {
13980
14134
  throw new Error(`source "${src}" is not a directory`);
13981
14135
  }
@@ -13984,13 +14138,20 @@ async function installPlugin(src, opts = {}) {
13984
14138
  }
13985
14139
  const { manifest: manifest2 } = loadPlugin(src);
13986
14140
  const { root, source } = installRoot(opts);
14141
+ const installed = discoverPluginCatalog({ cwd: opts.cwd }).plugins.filter(
14142
+ (item) => (opts.project ? item.source !== "user" : item.source === "user") && item.manifest.id !== manifest2.id
14143
+ );
14144
+ assertUniqueNpmPlugins([
14145
+ ...installed.map((item) => ({ alias: item.manifest.id, manifest: item.manifest })),
14146
+ { alias: manifest2.id, manifest: manifest2 }
14147
+ ]);
13987
14148
  const dest = (0, import_node_path11.join)(root, manifest2.id);
13988
14149
  if ((0, import_node_fs11.existsSync)(dest)) {
13989
14150
  if (!opts.force) {
13990
14151
  throw new Error(`plugin "${manifest2.id}" is already installed at ${dest} (use --force to overwrite)`);
13991
14152
  }
13992
14153
  }
13993
- const deps = pluginNpmDependencies(manifest2).length ? await installNodeDeps(src, { ...opts, cacheOnly: opts.cacheOnly ?? !opts.project }) : {};
14154
+ const deps = pluginNpmDependencies(manifest2).length ? await installNodeDepsUnlocked(src, { ...opts, cacheOnly: opts.cacheOnly ?? !opts.project }, !!opts.project) : {};
13994
14155
  if (deps.depsError) return { manifest: manifest2, dir: dest, source, ...deps };
13995
14156
  if ((0, import_node_fs11.existsSync)(dest)) (0, import_node_fs11.rmSync)(dest, { recursive: true, force: true });
13996
14157
  (0, import_node_fs11.mkdirSync)(root, { recursive: true });
@@ -14000,7 +14161,7 @@ async function installPlugin(src, opts = {}) {
14000
14161
  dir: dest,
14001
14162
  source,
14002
14163
  ...deps,
14003
- ...pluginNpmDependencies(manifest2).length ? {} : await installNodeDeps(dest, opts)
14164
+ ...pluginNpmDependencies(manifest2).length ? {} : await installNodeDepsUnlocked(dest, opts, !!opts.project)
14004
14165
  };
14005
14166
  }
14006
14167
 
@@ -14346,7 +14507,20 @@ var RemoteRegistry = class {
14346
14507
  }
14347
14508
  try {
14348
14509
  await withDependencyInstall(opts.cwd ?? process.cwd(), async () => {
14349
- const previous = readLockfile(opts.cwd).plugins[alias];
14510
+ const pins = readLockfile(opts.cwd).plugins;
14511
+ assertUniqueNpmPlugins([
14512
+ ...discoverPluginCatalog({ cwd: opts.cwd, storeRoot: opts.storeRoot }).plugins.filter(
14513
+ (item) => item.source === "project" && item.manifest.id !== alias && !pins[item.manifest.id]
14514
+ ).map((item) => ({ alias: item.manifest.id, manifest: item.manifest })),
14515
+ ...Object.entries(pins).filter(([key]) => key !== alias).map(([key, locked]) => ({
14516
+ alias: key,
14517
+ manifest: loadPluginManifest(
14518
+ storeEntryDir(locked.pluginId ?? key, locked.version, locked.sha256, opts.storeRoot)
14519
+ )
14520
+ })),
14521
+ { alias, manifest: entry.manifest }
14522
+ ]);
14523
+ const previous = pins[alias];
14350
14524
  if (previous && ((previous.pluginId ?? alias) !== entry.id || previous.registry !== this.baseUrl || previous.publisher !== pin.publisher))
14351
14525
  throw new Error("Plugin alias belongs to another publisher or registry");
14352
14526
  upsertLockEntry(opts.cwd ?? process.cwd(), alias, pin);