@myelinbridge/cli 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +27 -0
  2. package/bin/myelin.js +73 -1
  3. package/package.json +23 -23
package/README.md CHANGED
@@ -13,6 +13,8 @@ npx @myelinbridge/cli ping # verifies auth, prints your projects
13
13
  npx @myelinbridge/cli datasets # what you can deliver to, and whose move it is
14
14
  npx @myelinbridge/cli contract --dataset onco1-wes # what is expected of your delivery
15
15
  npx @myelinbridge/cli sample-depth 1 --dataset onco1-wes # once, before your first submit
16
+ npx @myelinbridge/cli roles --dataset onco1-wes # which file plays which role
17
+ npx @myelinbridge/cli roles set samplesheet /metadata/samplesheet.csv --dataset onco1-wes
16
18
  npx @myelinbridge/cli check ./run_042 --dataset onco1-wes # validate BEFORE uploading a byte
17
19
  npx @myelinbridge/cli push ./run_042 --dataset onco1-wes --submit
18
20
  ```
@@ -27,6 +29,31 @@ npx @myelinbridge/cli push ./run_042 --dataset onco1-wes --submit
27
29
  deliveries — after that, re-asserting the current value still succeeds and only a
28
30
  *change* exits `2`. This is the only dataset field you can write.
29
31
 
32
+ - **Declare which file plays which role — whenever you like (0.11.0).** Quality
33
+ checks read files by ROLE (`samplesheet`, `checksum_manifest`, `qc_report`,
34
+ `subject_roster`, `capture_bed`), not by a filing convention you never agreed
35
+ to. `roles` shows what is declared and what Myelin detected in your
36
+ deliveries; `roles set <role> <path>` confirms a file; `roles none <role>`
37
+ states your dataset has no such file (a statement, not a gap). Nothing is
38
+ ever required to deliver — an undeclared role just means the checks that read
39
+ it report *"could not check"* instead of running, so declaring is how you
40
+ activate more checks before upload and catch problems before review does.
41
+ Unlike `sample-depth` it **never locks**, and it is idempotent, so a pipeline
42
+ can assert it on every run. The samplesheet is **sticky**: send it once and
43
+ every later delivery reconciles against it — a partial delivery (93 of 96) is
44
+ reported as a state, never as a failure.
45
+
46
+ - **A missing path in `roles set` is refused, not guessed (0.11.1).** Flags are
47
+ not positional arguments: `roles set samplesheet --dataset onco1-wes` (path
48
+ omitted) used to declare the samplesheet at the literal path `--dataset` and
49
+ print a tick. Every samplesheet check then reported *"could not check — the
50
+ declared file is not in the delivery"* on a delivery that was fine. The
51
+ command now exits with its usage line instead.
52
+
53
+ - **`--dataset` takes the slug in any case (0.11.0).** Slugs are matched
54
+ case-insensitively (`onco1-wes` and `ONCO1-WES` name the same dataset); an id
55
+ or an exact name works too.
56
+
30
57
  - **Resume = re-run.** `push` is idempotent: already-uploaded files are skipped
31
58
  (path + size), and within a large file, parts that already landed are skipped
32
59
  too (S3 multipart). Uploads go direct to storage over short-lived presigned
package/bin/myelin.js CHANGED
@@ -191,9 +191,15 @@ async function resolveDataset(ref) {
191
191
  die(`Dataset ${ref} not found in this key's scope`)
192
192
  }
193
193
  const me = expectOk(await api('GET', '/me'), 'auth')
194
+ // Slugs are matched case-insensitively: seeded datasets carry lowercase slugs
195
+ // while the portal generates uppercase ones, and the slug is a handle a human
196
+ // types — the case of the fleet should never be their problem.
197
+ const want = ref.toLowerCase()
194
198
  for (const p of me.projects) {
195
199
  const r = expectOk(await api('GET', `/projects/${p.id}/datasets`), 'datasets')
196
- const hit = r.datasets.find((d) => d.slug === ref || d.name === ref)
200
+ const hit = r.datasets.find(
201
+ (d) => (typeof d.slug === 'string' && d.slug.toLowerCase() === want) || d.name === ref,
202
+ )
197
203
  if (hit) return hit
198
204
  }
199
205
  die(`No dataset with slug or name "${ref}" in this key's scope`)
@@ -896,6 +902,69 @@ async function cmdSampleDepth() {
896
902
  }
897
903
  }
898
904
 
905
+ // Which delivered file plays each role (sprint 19). Checks read files by ROLE
906
+ // — declaring one activates every check that was waiting on it, and nothing
907
+ // here is ever required to deliver. Unlike sample-depth this never locks:
908
+ // declarations apply forward, and re-declaring is allowed for the life of the
909
+ // dataset (idempotent — assert it on every run).
910
+ async function cmdRoles() {
911
+ const dsRef = opt('dataset')
912
+ if (!dsRef) die('Usage: myelin roles [set <role> <path> | none <role> | clear <role>] --dataset <slug|id>')
913
+ const dataset = await resolveDataset(dsRef)
914
+ const sub = args[1]
915
+
916
+ if (sub === 'set' || sub === 'none' || sub === 'clear') {
917
+ const role = args[2]
918
+ if (!role) die(`Usage: myelin roles ${sub} <role>${sub === 'set' ? ' <path>' : ''} --dataset <slug|id>`)
919
+ const body = { role }
920
+ if (sub === 'set') {
921
+ const path = args[3]
922
+ // ⚠ `args` only strips --json, so an omitted path lets the NEXT FLAG slide
923
+ // into its place: `roles set samplesheet --dataset onco1-wes` used to
924
+ // declare the samplesheet at `/--dataset`. The API accepts it (printable,
925
+ // under 255 chars), the CLI printed a tick, and every samplesheet check
926
+ // then answered role_file_missing on a delivery that was fine.
927
+ if (!path || path.startsWith('--')) {
928
+ die('Usage: myelin roles set <role> <path> --dataset <slug|id>')
929
+ }
930
+ body.path = path
931
+ } else if (sub === 'none') {
932
+ body.none = true
933
+ } else {
934
+ body.clear = true
935
+ }
936
+ const j = expectOk(await api('PUT', `/datasets/${dataset.id}/file-roles`, body), 'file-roles')
937
+ emit(j)
938
+ if (!JSON_MODE) {
939
+ out(
940
+ j.changed
941
+ ? `✓ ${role} ${sub === 'set' ? `→ ${body.path}` : sub === 'none' ? 'declared: no such file in this dataset' : 'declaration cleared'} for ${dataset.name}.`
942
+ : `${role} already declared that way for ${dataset.name} — nothing to change.`,
943
+ )
944
+ }
945
+ return
946
+ }
947
+
948
+ const j = expectOk(await api('GET', `/datasets/${dataset.id}/file-roles`), 'file-roles')
949
+ emit(j)
950
+ if (JSON_MODE) return
951
+ out(`${dataset.name} — delivery file roles`)
952
+ const declaredBy = new Map(j.declarations.map((d) => [d.role, d]))
953
+ for (const role of ['samplesheet', 'checksum_manifest', 'qc_report', 'subject_roster', 'capture_bed']) {
954
+ const d = declaredBy.get(role)
955
+ if (d) {
956
+ out(` ${role.padEnd(18)} ${d.declared_none ? '(no such file in this dataset — declared)' : d.path}`)
957
+ } else {
958
+ const cands = j.candidates.filter((c) => c.role === role)
959
+ out(` ${role.padEnd(18)} not declared${cands.length ? ` — looks like: ${cands.map((c) => c.path).join(', ')}` : ''}`)
960
+ }
961
+ }
962
+ const undeclared = 5 - j.declarations.length
963
+ if (undeclared > 0) {
964
+ out(`\nDeclaring a role activates the checks that read it — run: myelin roles set <role> <path> --dataset ${dsRef}`)
965
+ }
966
+ }
967
+
899
968
  function cmdHelp() {
900
969
  console.log(`myelin — Partner Ingestion CLI
901
970
 
@@ -910,6 +979,8 @@ Commands:
910
979
  projects list scoped projects
911
980
  datasets list datasets with "your move" hints
912
981
  sample-depth <0-5> --dataset <slug|id> declare the folder depth a sample sits at (locks after 1st submit)
982
+ roles [set|none|clear …] --dataset <…> which file plays each role (samplesheet, checksum list, …) —
983
+ declaring one activates the checks that read it; never locks
913
984
  contract --dataset <slug|id> what this dataset expects of your delivery
914
985
  check <dir> --dataset <slug|id> preflight local files against quality rules (no upload)
915
986
  push <dir> --dataset <slug|id> create/resume a delivery and upload (resumable; re-run to resume)
@@ -1008,6 +1079,7 @@ const commands = {
1008
1079
  projects: cmdProjects,
1009
1080
  datasets: cmdDatasets,
1010
1081
  'sample-depth': cmdSampleDepth,
1082
+ roles: cmdRoles,
1011
1083
  contract: cmdContract,
1012
1084
  check: cmdCheck,
1013
1085
  push: cmdPush,
package/package.json CHANGED
@@ -1,23 +1,23 @@
1
- {
2
- "name": "@myelinbridge/cli",
3
- "version": "0.10.0",
4
- "description": "Myelin Partner Ingestion CLI — push R&D data deliveries from a pipeline: preflight against the client's quality rules, resumable upload, submit, track review outcomes.",
5
- "type": "module",
6
- "bin": {
7
- "myelin": "bin/myelin.js"
8
- },
9
- "files": [
10
- "bin/",
11
- "README.md"
12
- ],
13
- "engines": {
14
- "node": ">=20"
15
- },
16
- "keywords": [
17
- "myelin",
18
- "pharma",
19
- "ingestion",
20
- "s3"
21
- ],
22
- "license": "UNLICENSED"
23
- }
1
+ {
2
+ "name": "@myelinbridge/cli",
3
+ "version": "0.11.1",
4
+ "description": "Myelin Partner Ingestion CLI — push R&D data deliveries from a pipeline: preflight against the client's quality rules, resumable upload, submit, track review outcomes.",
5
+ "type": "module",
6
+ "bin": {
7
+ "myelin": "bin/myelin.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "keywords": [
17
+ "myelin",
18
+ "pharma",
19
+ "ingestion",
20
+ "s3"
21
+ ],
22
+ "license": "UNLICENSED"
23
+ }