@gjsify/cli 0.6.1 → 0.7.2
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/dist/cli.gjs.mjs +14 -12
- package/lib/cli-app.js +2 -1
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/install.js +135 -106
- package/lib/commands/trust.d.ts +16 -0
- package/lib/commands/trust.js +306 -0
- package/lib/utils/prompt.js +6 -3
- package/lib/utils/trust-registry.d.ts +71 -0
- package/lib/utils/trust-registry.js +110 -0
- package/package.json +17 -17
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** `<registry>/-/package/<@scope%2fname>/trust`. */
|
|
2
|
+
export declare function trustUrl(registry: string, pkgName: string): string;
|
|
3
|
+
/** `<trustUrl>/<id>` for revoke (the id is path-encoded). */
|
|
4
|
+
export declare function trustRevokeUrl(registry: string, pkgName: string, id: string): string;
|
|
5
|
+
export interface GithubTrustClaims {
|
|
6
|
+
repository: string;
|
|
7
|
+
workflow_ref: {
|
|
8
|
+
file: string;
|
|
9
|
+
};
|
|
10
|
+
environment?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const PERMISSION_PUBLISH = "createPackage";
|
|
13
|
+
export declare const PERMISSION_STAGE_PUBLISH = "createStagedPackage";
|
|
14
|
+
export interface GithubTrustConfig {
|
|
15
|
+
type: 'github';
|
|
16
|
+
claims: GithubTrustClaims;
|
|
17
|
+
permissions: string[];
|
|
18
|
+
}
|
|
19
|
+
/** A single entry as returned by the list endpoint. */
|
|
20
|
+
export interface TrustEntry {
|
|
21
|
+
id?: string;
|
|
22
|
+
type?: string;
|
|
23
|
+
claims?: {
|
|
24
|
+
repository?: string;
|
|
25
|
+
workflow_ref?: {
|
|
26
|
+
file?: string;
|
|
27
|
+
};
|
|
28
|
+
environment?: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The POST body for `trust github`. npm always sends an ARRAY with a single
|
|
33
|
+
* config object carrying `type`, `claims` AND `permissions` (the last is
|
|
34
|
+
* required — `--allow-publish` → `['createPackage']`).
|
|
35
|
+
*/
|
|
36
|
+
export declare function githubTrustBody(opts: {
|
|
37
|
+
repository: string;
|
|
38
|
+
workflow: string;
|
|
39
|
+
environment?: string;
|
|
40
|
+
permissions?: string[];
|
|
41
|
+
}): GithubTrustConfig[];
|
|
42
|
+
/** npm requires the workflow as a bare basename (no directory), `.yml`/`.yaml`. */
|
|
43
|
+
export declare function normalizeWorkflowFile(wf: string): string;
|
|
44
|
+
/** `owner/repo` — exactly two non-empty segments. */
|
|
45
|
+
export declare function validateRepository(repo: string): boolean;
|
|
46
|
+
/** True when an existing trust entry already grants THIS repo + workflow. */
|
|
47
|
+
export declare function matchesGithubTrust(entry: TrustEntry, opts: {
|
|
48
|
+
repository: string;
|
|
49
|
+
workflow: string;
|
|
50
|
+
}): boolean;
|
|
51
|
+
export type TrustState = 'trusted' | 'untrusted' | 'unpublished' | 'auth-required' | 'error';
|
|
52
|
+
/**
|
|
53
|
+
* Classify a `trust list` response against the desired repo+workflow.
|
|
54
|
+
* 404 → unpublished (package not on the registry yet)
|
|
55
|
+
* 401 → auth-required (OTP / lapsed 2FA-skip window — can't tell the state)
|
|
56
|
+
* 2xx + array with a matching github entry → trusted
|
|
57
|
+
* 2xx + array without a match → untrusted (publish, but not for us)
|
|
58
|
+
* anything else → error
|
|
59
|
+
*/
|
|
60
|
+
export declare function classifyTrustList(status: number, body: unknown, opts: {
|
|
61
|
+
repository: string;
|
|
62
|
+
workflow: string;
|
|
63
|
+
}): TrustState;
|
|
64
|
+
/**
|
|
65
|
+
* Derive `owner/repo` from a git remote URL. Handles the common GitHub forms:
|
|
66
|
+
* git@github.com:owner/repo.git
|
|
67
|
+
* ssh://git@github.com/owner/repo.git
|
|
68
|
+
* https://github.com/owner/repo(.git)
|
|
69
|
+
* Returns null when it can't confidently extract two segments.
|
|
70
|
+
*/
|
|
71
|
+
export declare function parseRepoFromGitRemote(url: string): string | null;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Pure helpers for the npm "Trusted Publishers" (OIDC) registry API, used by
|
|
2
|
+
// `gjsify trust`. The wire contract mirrors npm's `trust` command (npm >= 11.10):
|
|
3
|
+
//
|
|
4
|
+
// list: GET <registry>/-/package/<escaped>/trust -> [{id,type,claims}]
|
|
5
|
+
// github: POST <registry>/-/package/<escaped>/trust body: [{type,claims}]
|
|
6
|
+
// revoke: DELETE <registry>/-/package/<escaped>/trust/<id>
|
|
7
|
+
//
|
|
8
|
+
// Everything here is side-effect-free so it can be unit-tested without a
|
|
9
|
+
// network; the command (commands/trust.ts) does the fetch + OTP orchestration.
|
|
10
|
+
// Reference: refs/npm-cli/lib/trust-cmd.js + lib/commands/trust/*.js.
|
|
11
|
+
import { escapePackageName } from './publish-headers.js';
|
|
12
|
+
/** `<registry>/-/package/<@scope%2fname>/trust`. */
|
|
13
|
+
export function trustUrl(registry, pkgName) {
|
|
14
|
+
const base = registry.endsWith('/') ? registry.slice(0, -1) : registry;
|
|
15
|
+
return `${base}/-/package/${escapePackageName(pkgName)}/trust`;
|
|
16
|
+
}
|
|
17
|
+
/** `<trustUrl>/<id>` for revoke (the id is path-encoded). */
|
|
18
|
+
export function trustRevokeUrl(registry, pkgName, id) {
|
|
19
|
+
return `${trustUrl(registry, pkgName)}/${encodeURIComponent(id)}`;
|
|
20
|
+
}
|
|
21
|
+
// Registry permission tokens (npm >= 11.10, the 2026-05-20 trust-API change).
|
|
22
|
+
// `createPackage` is what `--allow-publish` grants; `createStagedPackage` is
|
|
23
|
+
// `--allow-stage-publish`. The registry rejects a config with no permissions
|
|
24
|
+
// (`400 "permissions is required and must contain at least one valid route"`).
|
|
25
|
+
export const PERMISSION_PUBLISH = 'createPackage';
|
|
26
|
+
export const PERMISSION_STAGE_PUBLISH = 'createStagedPackage';
|
|
27
|
+
/**
|
|
28
|
+
* The POST body for `trust github`. npm always sends an ARRAY with a single
|
|
29
|
+
* config object carrying `type`, `claims` AND `permissions` (the last is
|
|
30
|
+
* required — `--allow-publish` → `['createPackage']`).
|
|
31
|
+
*/
|
|
32
|
+
export function githubTrustBody(opts) {
|
|
33
|
+
const claims = {
|
|
34
|
+
repository: opts.repository,
|
|
35
|
+
workflow_ref: { file: normalizeWorkflowFile(opts.workflow) },
|
|
36
|
+
};
|
|
37
|
+
if (opts.environment)
|
|
38
|
+
claims.environment = opts.environment;
|
|
39
|
+
const permissions = opts.permissions && opts.permissions.length > 0 ? opts.permissions : [PERMISSION_PUBLISH];
|
|
40
|
+
return [{ type: 'github', claims, permissions }];
|
|
41
|
+
}
|
|
42
|
+
/** npm requires the workflow as a bare basename (no directory), `.yml`/`.yaml`. */
|
|
43
|
+
export function normalizeWorkflowFile(wf) {
|
|
44
|
+
const base = wf.split('/').pop() ?? wf;
|
|
45
|
+
return base;
|
|
46
|
+
}
|
|
47
|
+
/** `owner/repo` — exactly two non-empty segments. */
|
|
48
|
+
export function validateRepository(repo) {
|
|
49
|
+
const parts = repo.split('/');
|
|
50
|
+
return parts.length === 2 && parts[0].length > 0 && parts[1].length > 0;
|
|
51
|
+
}
|
|
52
|
+
/** True when an existing trust entry already grants THIS repo + workflow. */
|
|
53
|
+
export function matchesGithubTrust(entry, opts) {
|
|
54
|
+
if (entry.type !== 'github')
|
|
55
|
+
return false;
|
|
56
|
+
const repo = entry.claims?.repository;
|
|
57
|
+
const file = entry.claims?.workflow_ref?.file;
|
|
58
|
+
return repo === opts.repository && file === normalizeWorkflowFile(opts.workflow);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Classify a `trust list` response against the desired repo+workflow.
|
|
62
|
+
* 404 → unpublished (package not on the registry yet)
|
|
63
|
+
* 401 → auth-required (OTP / lapsed 2FA-skip window — can't tell the state)
|
|
64
|
+
* 2xx + array with a matching github entry → trusted
|
|
65
|
+
* 2xx + array without a match → untrusted (publish, but not for us)
|
|
66
|
+
* anything else → error
|
|
67
|
+
*/
|
|
68
|
+
export function classifyTrustList(status, body, opts) {
|
|
69
|
+
if (status === 404)
|
|
70
|
+
return 'unpublished';
|
|
71
|
+
if (status === 401)
|
|
72
|
+
return 'auth-required';
|
|
73
|
+
if (status >= 200 && status < 300) {
|
|
74
|
+
const entries = Array.isArray(body) ? body : [];
|
|
75
|
+
return entries.some((e) => matchesGithubTrust(e, opts)) ? 'trusted' : 'untrusted';
|
|
76
|
+
}
|
|
77
|
+
return 'error';
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Derive `owner/repo` from a git remote URL. Handles the common GitHub forms:
|
|
81
|
+
* git@github.com:owner/repo.git
|
|
82
|
+
* ssh://git@github.com/owner/repo.git
|
|
83
|
+
* https://github.com/owner/repo(.git)
|
|
84
|
+
* Returns null when it can't confidently extract two segments.
|
|
85
|
+
*/
|
|
86
|
+
export function parseRepoFromGitRemote(url) {
|
|
87
|
+
let s = url.trim();
|
|
88
|
+
if (!s)
|
|
89
|
+
return null;
|
|
90
|
+
// Strip a trailing .git
|
|
91
|
+
s = s.replace(/\.git$/, '');
|
|
92
|
+
// scp-like: git@host:owner/repo
|
|
93
|
+
const scp = s.match(/^[^/@]+@[^:]+:(.+)$/);
|
|
94
|
+
if (scp) {
|
|
95
|
+
return twoSegments(scp[1]);
|
|
96
|
+
}
|
|
97
|
+
// URL forms: ssh://, https://, http://, git://
|
|
98
|
+
const m = s.match(/^[a-z]+:\/\/[^/]+\/(.+)$/i);
|
|
99
|
+
if (m) {
|
|
100
|
+
return twoSegments(m[1]);
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function twoSegments(path) {
|
|
105
|
+
const parts = path.split('/').filter((p) => p.length > 0);
|
|
106
|
+
if (parts.length < 2)
|
|
107
|
+
return null;
|
|
108
|
+
// owner/repo are the LAST two segments (handles nested groups → take repo + its owner)
|
|
109
|
+
return `${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
|
|
110
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -120,19 +120,19 @@
|
|
|
120
120
|
"cli"
|
|
121
121
|
],
|
|
122
122
|
"dependencies": {
|
|
123
|
-
"@gjsify/buffer": "^0.
|
|
124
|
-
"@gjsify/create-app": "^0.
|
|
125
|
-
"@gjsify/node-globals": "^0.
|
|
126
|
-
"@gjsify/node-polyfills": "^0.
|
|
127
|
-
"@gjsify/npm-registry": "^0.
|
|
128
|
-
"@gjsify/resolve-npm": "^0.
|
|
129
|
-
"@gjsify/rolldown-plugin-gjsify": "^0.
|
|
130
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
131
|
-
"@gjsify/semver": "^0.
|
|
132
|
-
"@gjsify/tar": "^0.
|
|
133
|
-
"@gjsify/tsc": "^0.
|
|
134
|
-
"@gjsify/web-polyfills": "^0.
|
|
135
|
-
"@gjsify/workspace": "^0.
|
|
123
|
+
"@gjsify/buffer": "^0.7.2",
|
|
124
|
+
"@gjsify/create-app": "^0.7.2",
|
|
125
|
+
"@gjsify/node-globals": "^0.7.2",
|
|
126
|
+
"@gjsify/node-polyfills": "^0.7.2",
|
|
127
|
+
"@gjsify/npm-registry": "^0.7.2",
|
|
128
|
+
"@gjsify/resolve-npm": "^0.7.2",
|
|
129
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.7.2",
|
|
130
|
+
"@gjsify/rolldown-plugin-pnp": "^0.7.2",
|
|
131
|
+
"@gjsify/semver": "^0.7.2",
|
|
132
|
+
"@gjsify/tar": "^0.7.2",
|
|
133
|
+
"@gjsify/tsc": "^0.7.2",
|
|
134
|
+
"@gjsify/web-polyfills": "^0.7.2",
|
|
135
|
+
"@gjsify/workspace": "^0.7.2",
|
|
136
136
|
"cosmiconfig": "^9.0.2",
|
|
137
137
|
"get-tsconfig": "^4.14.0",
|
|
138
138
|
"pkg-types": "^2.3.1",
|
|
@@ -140,13 +140,13 @@
|
|
|
140
140
|
"yargs": "^18.0.0"
|
|
141
141
|
},
|
|
142
142
|
"devDependencies": {
|
|
143
|
-
"@gjsify/unit": "^0.
|
|
143
|
+
"@gjsify/unit": "^0.7.2",
|
|
144
144
|
"@types/yargs": "^17.0.35",
|
|
145
145
|
"typescript": "^6.0.3"
|
|
146
146
|
},
|
|
147
147
|
"peerDependencies": {
|
|
148
|
-
"@gjsify/oxfmt-native": "^0.
|
|
149
|
-
"@gjsify/rolldown-native": "^0.
|
|
148
|
+
"@gjsify/oxfmt-native": "^0.7.2",
|
|
149
|
+
"@gjsify/rolldown-native": "^0.7.2"
|
|
150
150
|
},
|
|
151
151
|
"peerDependenciesMeta": {
|
|
152
152
|
"@gjsify/oxfmt-native": {
|