Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: CI

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
- run: npm test
- run: npm pack --dry-run
- run: git diff --check
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
coverage/
*.tgz
.DS_Store
.env
.env.*
9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Contributing to Gapwise CLI

Gapwise CLI helps maintain university integrations across the [Gapwise ecosystem](https://github.com/GapwiseHQ). Open a focused pull request with the related issue, the campus data sources, and the checks you ran.

## Local checks

Requires Node.js 24 or newer. Run `npm test`, `npm pack --dry-run`, and `git diff --check` before opening a pull request. The tests use a small, committed workspace fixture so they run without sibling repositories. Also try the command against current `gapwise` and `data` checkouts when changing scaffolding or validation behavior.

Keep generated data empty until sources have been reviewed. Do not invent building identities, entrances, routes, or accessibility facts. Report vulnerabilities privately through [SECURITY.md](SECURITY.md).
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,29 @@
### Repeatable university integrations for one Gapwise product.

[![MIT](https://img.shields.io/badge/License-MIT-111111?style=for-the-badge)](LICENSE)
[![CI](https://github.com/GapwiseHQ/cli/actions/workflows/ci.yml/badge.svg)](https://github.com/GapwiseHQ/cli/actions/workflows/ci.yml)

</div>

## Purpose

The CLI scaffolds a university manifest entry, timetable adapter, test, and empty campus data files in the sibling `gapwise` and `data` repositories. It also creates the app's empty campus mirror and map catalog so the generic campus loader discovers the new ID. It never copies application screens. Generated data contains no invented buildings, entrances, or routes.
The CLI scaffolds a university manifest entry, timetable adapter, test, and empty campus data files in the sibling [`gapwise`](https://github.com/GapwiseHQ/gapwise) and [`data`](https://github.com/GapwiseHQ/data) repositories. It also creates the app's empty campus mirror and map catalog so the generic campus loader discovers the new ID. It never copies application screens. Generated data contains no invented buildings, entrances, or routes.

## Installation

Requires Node 24, Bun 1.3.14 for application tests, and sibling `gapwise` and `data` checkouts.
Requires Node 24, Bun 1.3.14 for application tests, and sibling `gapwise` and `data` checkouts. The canonical CLI source is [GapwiseHQ/cli](https://github.com/GapwiseHQ/cli). Install directly from that repository:

```sh
npm install -g .
# Or run without global installation:
node bin/gapwise.mjs university create example-university --dry-run
npm install -g github:GapwiseHQ/cli
gapwise university create example-university --dry-run
```

Set `GAPWISE_WORKSPACE` or pass `--workspace /path/to/workspace` if the repositories are elsewhere.
To work on the CLI itself, clone this repository and run `npm test` or `node bin/gapwise.mjs`. By default, commands expect `cli`, `gapwise`, and `data` as sibling directories. Set `GAPWISE_WORKSPACE` or pass `--workspace /path/to/workspace` to point to their parent directory.

## Usage

```sh
gapwise university create tmu --name "Toronto Metropolitan University" --short-name TMU
gapwise university create example-university --name "Example University" --short-name Example --dry-run
gapwise university validate carleton
gapwise university test carleton
gapwise university dev carleton
Expand All @@ -54,7 +54,7 @@ node bin/gapwise.mjs university create example-university --dry-run

## Contributing

Use a focused PR, cite data sources and their redistribution terms, and keep unknown access facts unknown. See the Gapwise contributor guide in `gapwise`.
Use a focused PR, cite data sources and their redistribution terms, and keep unknown access facts unknown. See [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md), and the [Gapwise documentation](https://docs.gapwise.ca).

## License

Expand Down
5 changes: 5 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Security Policy

Security fixes target the latest `main` branch. Report suspected vulnerabilities privately to `security@gapwise.ca` or through GitHub private vulnerability reporting for this repository when available. The public policy is at [gapwise.ca/security](https://gapwise.ca/security).

Include reproduction steps using test data and the expected impact. Do not put credentials, private timetables, tokens, or exploit details in a public issue. Do not access another person's data or disrupt production services while testing.
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
{
"name": "@gapwise/cli",
"version": "0.1.0",
"description": "University integration and campus data tooling for Gapwise",
"type": "module",
"license": "MIT",
"homepage": "https://docs.gapwise.ca",
"repository": {
"type": "git",
"url": "git+https://github.com/GapwiseHQ/cli.git"
},
"bugs": { "url": "https://github.com/GapwiseHQ/cli/issues" },
"keywords": ["gapwise", "university", "campus", "timetable", "cli"],
"bin": { "gapwise": "./bin/gapwise.mjs" },
"scripts": { "test": "node --test tests/*.test.mjs" },
"engines": { "node": ">=24" },
Expand Down
25 changes: 12 additions & 13 deletions tests/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { dirname, join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
import { universityPlan } from '../bin/gapwise.mjs';

const realWorkspace = resolve(import.meta.dirname, '../..');
const fixtureSource = resolve(import.meta.dirname, 'fixtures');
function fixture() {
const workspace = mkdtempSync(join(tmpdir(), 'gapwise-cli-'));
for (const file of [
Expand All @@ -17,7 +17,7 @@ function fixture() {
]) {
const target = join(workspace, file);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, readFileSync(join(realWorkspace, file)));
writeFileSync(target, readFileSync(join(fixtureSource, file)));
}
return workspace;
}
Expand All @@ -39,19 +39,19 @@ test('dry run shows integration and data files without changing the workspace',
test('scaffold creates adapter, manifest, test, and empty campus snapshots', () => {
const workspace = fixture();
try {
const plan = universityPlan('mcmaster', workspace, ['--name', 'McMaster University', '--short-name', 'McMaster']);
const plan = universityPlan('sample-university', workspace, ['--name', 'Sample University', '--short-name', 'Sample']);
assert.equal(plan.length, 11);
assert.ok(plan.every(([path]) => !path.includes('/src/components/') && !path.includes('/src/routes/')));
const result = spawnSync('node', ['bin/gapwise.mjs', 'university', 'create', 'mcmaster', '--name', 'McMaster University', '--short-name', 'McMaster', '--workspace', workspace],
const result = spawnSync('node', ['bin/gapwise.mjs', 'university', 'create', 'sample-university', '--name', 'Sample University', '--short-name', 'Sample', '--workspace', workspace],
{ cwd: resolve(import.meta.dirname, '..'), encoding: 'utf8' });
assert.equal(result.status, 0, result.stderr);
const manifest = JSON.parse(readFileSync(join(workspace, 'gapwise/universities.json')));
assert.equal(manifest.universities.at(-1).name, 'McMaster University');
assert.equal(manifest.universities.at(-1).name, 'Sample University');
assert.equal(manifest.universities.at(-1).status, 'scaffold');
assert.deepEqual(JSON.parse(readFileSync(join(workspace, 'data/universities/mcmaster/campus.json'))).entrances, []);
assert.deepEqual(JSON.parse(readFileSync(join(workspace, 'gapwise/src/data/campuses/mcmaster/catalog.json'))).buildings, []);
assert.match(readFileSync(join(workspace, 'gapwise/src/data/campuses/index.ts'), 'utf8'), /"mcmaster": mcmasterCatalogRaw/);
assert.deepEqual(readdirSync(join(workspace, 'gapwise/src/universities/mcmaster')).sort(), ['adapter.test.ts', 'adapter.ts', 'demo-timetable.ts']);
assert.deepEqual(JSON.parse(readFileSync(join(workspace, 'data/universities/sample-university/campus.json'))).entrances, []);
assert.deepEqual(JSON.parse(readFileSync(join(workspace, 'gapwise/src/data/campuses/sample-university/catalog.json'))).buildings, []);
assert.match(readFileSync(join(workspace, 'gapwise/src/data/campuses/index.ts'), 'utf8'), /"sample-university": sample_universityCatalogRaw/);
assert.deepEqual(readdirSync(join(workspace, 'gapwise/src/universities/sample-university')).sort(), ['adapter.test.ts', 'adapter.ts', 'demo-timetable.ts']);
} finally { rmSync(workspace, { recursive: true, force: true }); }
});

Expand All @@ -73,15 +73,14 @@ test('university validate detects scaffold state and unknown universities', () =
assert.notEqual(unknown.status, 0);
assert.match(unknown.stderr, /nonexistent is not registered/);

// Scaffold mcmaster
spawnSync('node', ['bin/gapwise.mjs', 'university', 'create', 'mcmaster', '--workspace', workspace],
// Scaffold an unregistered institution
spawnSync('node', ['bin/gapwise.mjs', 'university', 'create', 'sample-university', '--workspace', workspace],
{ cwd: resolve(import.meta.dirname, '..'), encoding: 'utf8' });

// Validating unactivated scaffold fails
const scaffoldCheck = spawnSync('node', ['bin/gapwise.mjs', 'university', 'validate', 'mcmaster', '--workspace', workspace],
const scaffoldCheck = spawnSync('node', ['bin/gapwise.mjs', 'university', 'validate', 'sample-university', '--workspace', workspace],
{ cwd: resolve(import.meta.dirname, '..'), encoding: 'utf8' });
assert.notEqual(scaffoldCheck.status, 0);
assert.match(scaffoldCheck.stderr, /still a scaffold/);
} finally { rmSync(workspace, { recursive: true, force: true }); }
});

23 changes: 23 additions & 0 deletions tests/fixtures/data/schemas/universities/campus.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema":"http://json-schema.org/draft-07/schema#",
"type":"object","additionalProperties":false,
"required":["schemaVersion","institution","campus","sources","buildings","entrances","pathNodes","pathEdges"],
"properties":{
"schemaVersion":{"const":1},"institution":{"type":"string","pattern":"^[a-z][a-z0-9-]*$"},
"campus":{"type":"object","additionalProperties":false,"required":["id","name","bounds"],"properties":{"id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"bounds":{"anyOf":[{"type":"null"},{"type":"array","items":{"$ref":"#/$defs/coordinate"},"minItems":2,"maxItems":2}]}}},
"sources":{"type":"array","items":{"$ref":"#/$defs/source"}},
"buildings":{"type":"array","items":{"$ref":"#/$defs/building"}},
"entrances":{"type":"array","items":{"$ref":"#/$defs/entrance"}},
"pathNodes":{"type":"array","items":{"$ref":"#/$defs/node"}},
"pathEdges":{"type":"array","items":{"$ref":"#/$defs/edge"}}
},
"$defs":{
"coordinate":{"type":"array","items":[{"type":"number","minimum":-180,"maximum":180},{"type":"number","minimum":-90,"maximum":90}],"additionalItems":false},
"source":{"type":"object","additionalProperties":false,"required":["id","title","url","retrievedAt","licenseOrTerms","redistribution","transformation"],"properties":{"id":{"type":"string","minLength":1},"title":{"type":"string","minLength":1},"url":{"type":"string","format":"uri"},"retrievedAt":{"type":"string","format":"date"},"licenseOrTerms":{"type":"string","minLength":1},"redistribution":{"enum":["permitted","unclear","prohibited"]},"transformation":{"type":"string","minLength":1},"attribution":{"type":"string"}}},
"provenance":{"type":"object","additionalProperties":false,"required":["sourceId","nativeId","verification"],"properties":{"sourceId":{"type":"string","minLength":1},"nativeId":{"type":["string","null"]},"verification":{"enum":["source-backed","field-reviewed","inferred"]}}},
"building":{"type":"object","additionalProperties":false,"required":["id","name","nativeCodes","aliases","geometry","provenance"],"properties":{"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]*$"},"name":{"type":"string","minLength":1},"nativeCodes":{"type":"array","items":{"type":"string","minLength":1}},"aliases":{"type":"array","items":{"type":"string","minLength":1}},"category":{"enum":["academic","residence","facility"]},"geometry":{"anyOf":[{"type":"null"},{"type":"object","additionalProperties":false,"required":["type","coordinates"],"properties":{"type":{"enum":["Polygon","MultiPolygon"]},"coordinates":{"type":"array","minItems":1}}}]},"provenance":{"type":"array","minItems":1,"items":{"$ref":"#/$defs/provenance"}}}},
"entrance":{"type":"object","additionalProperties":false,"required":["id","buildingId","coordinate","pathNodeId","access","provenance"],"properties":{"id":{"type":"string","minLength":1},"buildingId":{"type":"string","minLength":1},"coordinate":{"$ref":"#/$defs/coordinate"},"pathNodeId":{"type":"string","minLength":1},"access":{"enum":["public","restricted","unknown"]},"provenance":{"type":"array","minItems":1,"items":{"$ref":"#/$defs/provenance"}}}},
"node":{"type":"object","additionalProperties":false,"required":["id","coordinate","provenance"],"properties":{"id":{"type":"string","minLength":1},"coordinate":{"$ref":"#/$defs/coordinate"},"provenance":{"type":"array","minItems":1,"items":{"$ref":"#/$defs/provenance"}}}},
"edge":{"type":"object","additionalProperties":false,"required":["id","from","to","mode","provenance"],"properties":{"id":{"type":"string","minLength":1},"from":{"type":"string","minLength":1},"to":{"type":"string","minLength":1},"mode":{"enum":["outdoor-walk","tunnel-walk"]},"provenance":{"type":"array","minItems":1,"items":{"$ref":"#/$defs/provenance"}}}}
}
}
Loading
Loading