@orchard-ai/cli 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # @orchard-ai/cli
2
+
3
+ Public npm installer for the Orchard CLI native binary.
4
+
5
+ ```bash
6
+ npm install -g @orchard-ai/cli
7
+ orchard --help
8
+ ```
9
+
10
+ This package does not contain Orchard CLI source code. During `postinstall`, it
11
+ downloads the platform-specific `orchard` binary from the latest `cli/v*`
12
+ release, verifies it against `SHA256SUMS`, and stores it under `vendor/`.
13
+
14
+ ## Configuration
15
+
16
+ - `ORCHARD_CLI_RELEASE_REPO`: GitHub repo that hosts public binary releases.
17
+ Defaults to `Orchard-Development/CLI`.
18
+ - `ORCHARD_CLI_RELEASES_URL`: full GitHub-compatible releases API URL override.
19
+ - `ORCHARD_CLI_RELEASE_TOKEN` or `GITHUB_TOKEN`: optional token for non-public
20
+ release assets.
21
+ - `ORCHARD_CLI_SKIP_DOWNLOAD=1`: skip download, useful for package inspection.
22
+
23
+ ## Source Privacy
24
+
25
+ The published package is intentionally limited to:
26
+
27
+ - `bin/orchard.js`
28
+ - `scripts/install.js`
29
+ - `scripts/verify-package.js`
30
+ - `vendor/.gitkeep`
31
+ - package metadata and this README
32
+
33
+ `npm run test` and `npm pack --dry-run` should be run before publishing to verify
34
+ that no Python source, source maps, repository metadata, or build artifacts are
35
+ included.
36
+
37
+ ## GitHub Actions Publishing
38
+
39
+ The `cli/v*` release workflow publishes this package when the GitHub Actions
40
+ secret `NPM_TOKEN` is available.
41
+
42
+ Recommended setup:
43
+
44
+ 1. Create an npm granular automation token with publish access to
45
+ `@orchard-ai/cli`.
46
+ 2. In GitHub, add an organization Actions secret:
47
+ `Orchard-Development` -> `Settings` -> `Secrets and variables` ->
48
+ `Actions` -> `New organization secret`.
49
+ 3. Name the secret `NPM_TOKEN`.
50
+ 4. Grant repository access to the private `CLI` repo, or set visibility to all
51
+ private repositories if Orchard intentionally shares the token across release
52
+ workflows.
53
+
54
+ CLI equivalent for an org admin:
55
+
56
+ ```bash
57
+ gh secret set NPM_TOKEN \
58
+ --org Orchard-Development \
59
+ --app actions \
60
+ --repos CLI
61
+ ```
62
+
63
+ The package is public by design, but the npm tarball contains only this installer
64
+ wrapper. Orchard CLI source remains private.
package/bin/orchard.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ const root = path.resolve(__dirname, '..');
9
+ const exe = process.platform === 'win32' ? 'orchard.exe' : 'orchard';
10
+ const binary = path.join(root, 'vendor', exe);
11
+
12
+ if (!fs.existsSync(binary)) {
13
+ console.error(
14
+ 'Orchard CLI binary is not installed. Run `npm rebuild @orchard-ai/cli` ' +
15
+ 'or reinstall with `npm install -g @orchard-ai/cli`.'
16
+ );
17
+ process.exit(127);
18
+ }
19
+
20
+ const result = spawnSync(binary, process.argv.slice(2), {
21
+ stdio: 'inherit',
22
+ windowsHide: false,
23
+ });
24
+
25
+ if (result.error) {
26
+ console.error(result.error.message);
27
+ process.exit(1);
28
+ }
29
+
30
+ process.exit(result.status === null ? 1 : result.status);
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@orchard-ai/cli",
3
+ "version": "0.2.4",
4
+ "description": "Installer and launcher for the Orchard CLI native binary.",
5
+ "license": "UNLICENSED",
6
+ "homepage": "https://orchard.dev",
7
+ "bin": {
8
+ "orchard": "bin/orchard.js"
9
+ },
10
+ "scripts": {
11
+ "postinstall": "node scripts/install.js",
12
+ "prepack": "node scripts/verify-package.js",
13
+ "test": "node --check bin/orchard.js && node --check scripts/install.js && node --check scripts/verify-package.js && node scripts/verify-package.js"
14
+ },
15
+ "files": [
16
+ "bin/",
17
+ "scripts/",
18
+ "vendor/.gitkeep",
19
+ "README.md",
20
+ "package.json"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ }
28
+ }
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const crypto = require('crypto');
5
+ const fs = require('fs');
6
+ const https = require('https');
7
+ const path = require('path');
8
+
9
+ const root = path.resolve(__dirname, '..');
10
+ const vendorDir = path.join(root, 'vendor');
11
+ const releasesRepo = process.env.ORCHARD_CLI_RELEASE_REPO || 'Orchard-Development/CLI';
12
+ const releasesUrl =
13
+ process.env.ORCHARD_CLI_RELEASES_URL ||
14
+ `https://api.github.com/repos/${releasesRepo}/releases?per_page=20`;
15
+ const token = process.env.ORCHARD_CLI_RELEASE_TOKEN || process.env.GITHUB_TOKEN || '';
16
+
17
+ function log(message) {
18
+ console.log(`[orchard-cli] ${message}`);
19
+ }
20
+
21
+ function request(url, binary = false) {
22
+ return new Promise((resolve, reject) => {
23
+ const headers = {
24
+ 'User-Agent': '@orchard-ai/cli installer',
25
+ Accept: 'application/vnd.github+json, application/octet-stream',
26
+ };
27
+ if (token) headers.Authorization = `Bearer ${token}`;
28
+
29
+ https
30
+ .get(url, { headers }, (res) => {
31
+ if ([301, 302, 303, 307, 308].includes(res.statusCode)) {
32
+ res.resume();
33
+ request(res.headers.location, binary).then(resolve, reject);
34
+ return;
35
+ }
36
+ if (res.statusCode < 200 || res.statusCode >= 300) {
37
+ res.resume();
38
+ reject(new Error(`GET ${url} failed with HTTP ${res.statusCode}`));
39
+ return;
40
+ }
41
+
42
+ const chunks = [];
43
+ res.on('data', (chunk) => chunks.push(chunk));
44
+ res.on('end', () => {
45
+ const body = Buffer.concat(chunks);
46
+ resolve(binary ? body : body.toString('utf8'));
47
+ });
48
+ })
49
+ .on('error', reject);
50
+ });
51
+ }
52
+
53
+ function targetAssetName() {
54
+ const platform = process.platform;
55
+ const arch = process.arch;
56
+
57
+ if (platform === 'darwin' && arch === 'arm64') return 'orchard-macos-arm64';
58
+ if (platform === 'darwin' && arch === 'x64') return 'orchard-macos-x64';
59
+ if (platform === 'linux' && arch === 'x64') return 'orchard-linux-x64';
60
+ if (platform === 'win32' && arch === 'x64') return 'orchard-windows-x64.exe';
61
+
62
+ throw new Error(`Unsupported platform/arch: ${platform}/${arch}`);
63
+ }
64
+
65
+ function parseSha256Sums(text) {
66
+ const sums = new Map();
67
+ for (const line of text.split(/\r?\n/)) {
68
+ const trimmed = line.trim();
69
+ if (!trimmed) continue;
70
+ const parts = trimmed.split(/\s+/);
71
+ if (parts.length < 2) continue;
72
+ sums.set(path.basename(parts[parts.length - 1].replace(/^\*/, '')), parts[0].toLowerCase());
73
+ }
74
+ return sums;
75
+ }
76
+
77
+ function sha256(buffer) {
78
+ return crypto.createHash('sha256').update(buffer).digest('hex');
79
+ }
80
+
81
+ function findRelease(releases) {
82
+ if (!Array.isArray(releases)) throw new Error('Release API response was not an array');
83
+ const release = releases.find((item) => typeof item.tag_name === 'string' && item.tag_name.startsWith('cli/v'));
84
+ if (!release) throw new Error('No cli/v* Orchard CLI release found');
85
+ return release;
86
+ }
87
+
88
+ function findAsset(release, name) {
89
+ const asset = (release.assets || []).find((item) => item.name === name);
90
+ if (!asset || !asset.browser_download_url) {
91
+ throw new Error(`Release ${release.tag_name} has no asset named ${name}`);
92
+ }
93
+ return asset;
94
+ }
95
+
96
+ async function install() {
97
+ if (process.env.ORCHARD_CLI_SKIP_DOWNLOAD === '1') {
98
+ log('skipping binary download because ORCHARD_CLI_SKIP_DOWNLOAD=1');
99
+ return;
100
+ }
101
+
102
+ const wanted = targetAssetName();
103
+ const exe = process.platform === 'win32' ? 'orchard.exe' : 'orchard';
104
+ const target = path.join(vendorDir, exe);
105
+
106
+ fs.mkdirSync(vendorDir, { recursive: true });
107
+
108
+ log(`resolving latest cli/v* release from ${releasesRepo}`);
109
+ const releases = JSON.parse(await request(releasesUrl));
110
+ const release = findRelease(releases);
111
+ const binaryAsset = findAsset(release, wanted);
112
+ const sumsAsset = findAsset(release, 'SHA256SUMS');
113
+
114
+ log(`downloading ${binaryAsset.name} from ${release.tag_name}`);
115
+ const [binary, sumsText] = await Promise.all([
116
+ request(binaryAsset.browser_download_url, true),
117
+ request(sumsAsset.browser_download_url, false),
118
+ ]);
119
+
120
+ const expected = parseSha256Sums(sumsText).get(binaryAsset.name);
121
+ if (!expected) throw new Error(`SHA256SUMS has no entry for ${binaryAsset.name}`);
122
+
123
+ const actual = sha256(binary);
124
+ if (actual !== expected) {
125
+ throw new Error(`SHA256 mismatch for ${binaryAsset.name}: expected ${expected} got ${actual}`);
126
+ }
127
+
128
+ fs.writeFileSync(target, binary);
129
+ if (process.platform !== 'win32') fs.chmodSync(target, 0o755);
130
+ log(`installed ${target}`);
131
+ }
132
+
133
+ install().catch((error) => {
134
+ console.error(`[orchard-cli] install failed: ${error.message}`);
135
+ console.error('[orchard-cli] Set ORCHARD_CLI_RELEASE_REPO, ORCHARD_CLI_RELEASES_URL, or ORCHARD_CLI_RELEASE_TOKEN if the release assets are not public.');
136
+ process.exit(1);
137
+ });
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const root = path.resolve(__dirname, '..');
8
+ const allowedTopLevel = new Set(['bin', 'scripts', 'vendor', 'README.md', 'package.json']);
9
+ const forbiddenExtensions = new Set(['.py', '.pyc', '.map', '.ts', '.tsx']);
10
+ const forbiddenNames = new Set(['.git', '.github', '.venv', '__pycache__', 'orchard', 'tests']);
11
+
12
+ function fail(message) {
13
+ console.error(`[verify-package] ${message}`);
14
+ process.exit(1);
15
+ }
16
+
17
+ function walk(dir) {
18
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
19
+ const full = path.join(dir, entry.name);
20
+ const rel = path.relative(root, full);
21
+ const top = rel.split(path.sep)[0];
22
+
23
+ if (!allowedTopLevel.has(top)) fail(`unexpected top-level package path: ${rel}`);
24
+ if (forbiddenNames.has(entry.name)) fail(`forbidden package path: ${rel}`);
25
+ if (entry.isFile() && forbiddenExtensions.has(path.extname(entry.name))) {
26
+ fail(`forbidden source-like file in package: ${rel}`);
27
+ }
28
+ if (entry.isDirectory()) walk(full);
29
+ }
30
+ }
31
+
32
+ walk(root);
33
+ console.log('[verify-package] package allowlist ok');
@@ -0,0 +1 @@
1
+