@ozyman42/ozy-cli 0.4.9-linux-x64.0 → 0.4.9
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/LICENSE +21 -0
- package/README.md +8 -0
- package/bin/ozy +209 -0
- package/bin/ozy-signing-agent +209 -0
- package/bin/ozy-ssh-keygen +209 -0
- package/bin/ozy-virtual-security-key +209 -0
- package/dist/commands/git/hosts.js +18 -0
- package/dist/commands/git/hosts.js.map +1 -0
- package/dist/commands/git/index.js +8 -0
- package/dist/commands/git/index.js.map +1 -0
- package/dist/commands/git/setup.js +145 -0
- package/dist/commands/git/setup.js.map +1 -0
- package/dist/commands/index.js +13 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/npm/index.js +7 -0
- package/dist/commands/npm/index.js.map +1 -0
- package/dist/commands/npm/setup/git-remote.js +66 -0
- package/dist/commands/npm/setup/git-remote.js.map +1 -0
- package/dist/commands/npm/setup/index.js +168 -0
- package/dist/commands/npm/setup/index.js.map +1 -0
- package/dist/commands/npm/setup/npm-auth.js +62 -0
- package/dist/commands/npm/setup/npm-auth.js.map +1 -0
- package/dist/commands/npm/setup/package-json-validate.js +47 -0
- package/dist/commands/npm/setup/package-json-validate.js.map +1 -0
- package/dist/commands/npm/setup/package-registry.js +20 -0
- package/dist/commands/npm/setup/package-registry.js.map +1 -0
- package/dist/commands/npm/setup/setup.test.js +95 -0
- package/dist/commands/npm/setup/setup.test.js.map +1 -0
- package/dist/commands/npm/setup/trusted-publishing.js +84 -0
- package/dist/commands/npm/setup/trusted-publishing.js.map +1 -0
- package/dist/commands/npm/setup/workflow.js +19 -0
- package/dist/commands/npm/setup/workflow.js.map +1 -0
- package/dist/commands/ssh/get-sk-credential.js +104 -0
- package/dist/commands/ssh/get-sk-credential.js.map +1 -0
- package/dist/commands/ssh/index.js +7 -0
- package/dist/commands/ssh/index.js.map +1 -0
- package/dist/commands/upgrade.js +236 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/common/command.js +15 -0
- package/dist/common/command.js.map +1 -0
- package/dist/common/constants.js +25 -0
- package/dist/common/constants.js.map +1 -0
- package/dist/common/effective-modules-extensions.js +13 -0
- package/dist/common/effective-modules-extensions.js.map +1 -0
- package/dist/common/log.js +4 -0
- package/dist/common/log.js.map +1 -0
- package/dist/common/render-caller-tree.js +20 -0
- package/dist/common/render-caller-tree.js.map +1 -0
- package/dist/entrypoints/ozy-signing-agent.js +48 -0
- package/dist/entrypoints/ozy-signing-agent.js.map +1 -0
- package/dist/entrypoints/ozy-ssh-keygen.js +9 -0
- package/dist/entrypoints/ozy-ssh-keygen.js.map +1 -0
- package/dist/entrypoints/ozy-virtual-security-key.js +18 -0
- package/dist/entrypoints/ozy-virtual-security-key.js.map +1 -0
- package/dist/entrypoints/ozy.js +3 -0
- package/dist/entrypoints/ozy.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/thing.js +4 -0
- package/dist/internal/thing.js.map +1 -0
- package/dist/modules/cli/agent-client/impl.js +109 -0
- package/dist/modules/cli/agent-client/impl.js.map +1 -0
- package/dist/modules/cli/agent-client/interface.js +11 -0
- package/dist/modules/cli/agent-client/interface.js.map +1 -0
- package/dist/modules/cli/git/impl.js +124 -0
- package/dist/modules/cli/git/impl.js.map +1 -0
- package/dist/modules/cli/git/interface.js +3 -0
- package/dist/modules/cli/git/interface.js.map +1 -0
- package/dist/modules/cli/git/submodule/fix.js +257 -0
- package/dist/modules/cli/git/submodule/fix.js.map +1 -0
- package/dist/modules/cli/git/submodule/git-state.js +154 -0
- package/dist/modules/cli/git/submodule/git-state.js.map +1 -0
- package/dist/modules/cli/git/submodule/gitmodules.js +113 -0
- package/dist/modules/cli/git/submodule/gitmodules.js.map +1 -0
- package/dist/modules/cli/git/submodule/printer.js +232 -0
- package/dist/modules/cli/git/submodule/printer.js.map +1 -0
- package/dist/modules/cli/git/submodule/resolve-git-dir.js +16 -0
- package/dist/modules/cli/git/submodule/resolve-git-dir.js.map +1 -0
- package/dist/modules/cli/github/impl.js +137 -0
- package/dist/modules/cli/github/impl.js.map +1 -0
- package/dist/modules/cli/github/interface.js +2 -0
- package/dist/modules/cli/github/interface.js.map +1 -0
- package/dist/modules/cli/index.js +9 -0
- package/dist/modules/cli/index.js.map +1 -0
- package/dist/modules/common/crypto/impl.js +133 -0
- package/dist/modules/common/crypto/impl.js.map +1 -0
- package/dist/modules/common/crypto/interface.js +2 -0
- package/dist/modules/common/crypto/interface.js.map +1 -0
- package/dist/modules/common/index.js +10 -0
- package/dist/modules/common/index.js.map +1 -0
- package/dist/modules/common/kep-map-store/impl.js +42 -0
- package/dist/modules/common/kep-map-store/impl.js.map +1 -0
- package/dist/modules/common/kep-map-store/interface.js +2 -0
- package/dist/modules/common/kep-map-store/interface.js.map +1 -0
- package/dist/modules/common/os-platform/caller-info.js +132 -0
- package/dist/modules/common/os-platform/caller-info.js.map +1 -0
- package/dist/modules/common/os-platform/impl.js +95 -0
- package/dist/modules/common/os-platform/impl.js.map +1 -0
- package/dist/modules/common/os-platform/interface.js +2 -0
- package/dist/modules/common/os-platform/interface.js.map +1 -0
- package/dist/modules/common/os-platform/virtual-hid/index.js +12 -0
- package/dist/modules/common/os-platform/virtual-hid/index.js.map +1 -0
- package/dist/modules/common/os-platform/virtual-hid/linux.js +5 -0
- package/dist/modules/common/os-platform/virtual-hid/linux.js.map +1 -0
- package/dist/modules/common/os-platform/virtual-hid/mac.js +119 -0
- package/dist/modules/common/os-platform/virtual-hid/mac.js.map +1 -0
- package/dist/modules/common/os-platform/virtual-hid/windows.js +5 -0
- package/dist/modules/common/os-platform/virtual-hid/windows.js.map +1 -0
- package/dist/modules/common/ssh-config/impl.js +116 -0
- package/dist/modules/common/ssh-config/impl.js.map +1 -0
- package/dist/modules/common/ssh-config/interface.js +10 -0
- package/dist/modules/common/ssh-config/interface.js.map +1 -0
- package/dist/modules/ssh-agent/index.js +8 -0
- package/dist/modules/ssh-agent/index.js.map +1 -0
- package/dist/modules/ssh-agent/session/impl.js +265 -0
- package/dist/modules/ssh-agent/session/impl.js.map +1 -0
- package/dist/modules/ssh-agent/session/interface-rpc.js +20 -0
- package/dist/modules/ssh-agent/session/interface-rpc.js.map +1 -0
- package/dist/modules/ssh-agent/session/interface.js +32 -0
- package/dist/modules/ssh-agent/session/interface.js.map +1 -0
- package/dist/modules/ssh-agent/session/passkey-prf-page.js +204 -0
- package/dist/modules/ssh-agent/session/passkey-prf-page.js.map +1 -0
- package/dist/modules/ssh-agent/session/prf-flow.js +103 -0
- package/dist/modules/ssh-agent/session/prf-flow.js.map +1 -0
- package/dist/modules/ssh-agent/ssh-agent/impl.js +117 -0
- package/dist/modules/ssh-agent/ssh-agent/impl.js.map +1 -0
- package/dist/modules/ssh-agent/ssh-agent/interface.js +2 -0
- package/dist/modules/ssh-agent/ssh-agent/interface.js.map +1 -0
- package/dist/scripts/build.js +444 -0
- package/dist/scripts/build.js.map +1 -0
- package/dist/scripts/check-npm-version.js +8 -0
- package/dist/scripts/check-npm-version.js.map +1 -0
- package/dist/scripts/constants.js +12 -0
- package/dist/scripts/constants.js.map +1 -0
- package/dist/scripts/kill-existing-agent.js +21 -0
- package/dist/scripts/kill-existing-agent.js.map +1 -0
- package/dist/scripts/publish.js +83 -0
- package/dist/scripts/publish.js.map +1 -0
- package/install.js +209 -0
- package/package.json +58 -11
- package/src/commands/git/hosts.ts +20 -0
- package/src/commands/git/index.ts +9 -0
- package/src/commands/git/setup.ts +182 -0
- package/src/commands/index.ts +15 -0
- package/src/commands/npm/index.ts +8 -0
- package/src/commands/npm/setup/git-remote.ts +78 -0
- package/src/commands/npm/setup/index.ts +189 -0
- package/src/commands/npm/setup/npm-auth.ts +58 -0
- package/src/commands/npm/setup/package-json-validate.ts +68 -0
- package/src/commands/npm/setup/package-registry.ts +24 -0
- package/src/commands/npm/setup/publish-workflow.yml +35 -0
- package/src/commands/npm/setup/setup.test.ts +117 -0
- package/src/commands/npm/setup/trusted-publishing.ts +106 -0
- package/src/commands/npm/setup/workflow.ts +20 -0
- package/src/commands/ssh/get-sk-credential.ts +110 -0
- package/src/commands/ssh/index.ts +8 -0
- package/src/commands/upgrade.ts +261 -0
- package/src/common/command.ts +16 -0
- package/src/common/constants.ts +26 -0
- package/src/common/effective-modules-extensions.ts +24 -0
- package/src/common/log.ts +3 -0
- package/src/common/render-caller-tree.ts +22 -0
- package/src/entrypoints/ozy-signing-agent.ts +48 -0
- package/src/entrypoints/ozy-ssh-keygen.ts +10 -0
- package/src/entrypoints/ozy-virtual-security-key.ts +21 -0
- package/src/entrypoints/ozy.ts +2 -0
- package/src/index.ts +1 -0
- package/src/internal/thing.ts +3 -0
- package/src/modules/cli/agent-client/impl.ts +152 -0
- package/src/modules/cli/agent-client/interface.ts +17 -0
- package/src/modules/cli/git/impl.ts +133 -0
- package/src/modules/cli/git/interface.ts +23 -0
- package/src/modules/cli/git/submodule/fix.ts +298 -0
- package/src/modules/cli/git/submodule/git-state.ts +217 -0
- package/src/modules/cli/git/submodule/gitmodules.ts +126 -0
- package/src/modules/cli/git/submodule/printer.ts +256 -0
- package/src/modules/cli/git/submodule/resolve-git-dir.ts +14 -0
- package/src/modules/cli/github/impl.ts +169 -0
- package/src/modules/cli/github/interface.ts +29 -0
- package/src/modules/cli/index.ts +16 -0
- package/src/modules/common/crypto/impl.ts +173 -0
- package/src/modules/common/crypto/interface.ts +20 -0
- package/src/modules/common/index.ts +19 -0
- package/src/modules/common/kep-map-store/impl.ts +47 -0
- package/src/modules/common/kep-map-store/interface.ts +9 -0
- package/src/modules/common/os-platform/caller-info.ts +152 -0
- package/src/modules/common/os-platform/impl.ts +91 -0
- package/src/modules/common/os-platform/interface.ts +16 -0
- package/src/modules/common/os-platform/virtual-hid/index.ts +12 -0
- package/src/modules/common/os-platform/virtual-hid/linux.ts +7 -0
- package/src/modules/common/os-platform/virtual-hid/mac.ts +150 -0
- package/src/modules/common/os-platform/virtual-hid/windows.ts +7 -0
- package/src/modules/common/ssh-config/impl.ts +130 -0
- package/src/modules/common/ssh-config/interface.ts +33 -0
- package/src/modules/ssh-agent/index.ts +13 -0
- package/src/modules/ssh-agent/session/agent-notes.md +210 -0
- package/src/modules/ssh-agent/session/design.md +2 -0
- package/src/modules/ssh-agent/session/html-refactor.md +23 -0
- package/src/modules/ssh-agent/session/impl.ts +328 -0
- package/src/modules/ssh-agent/session/interface-rpc.ts +24 -0
- package/src/modules/ssh-agent/session/interface.ts +62 -0
- package/src/modules/ssh-agent/session/passkey-prf-page.ts +224 -0
- package/src/modules/ssh-agent/session/prf-flow.ts +152 -0
- package/src/modules/ssh-agent/ssh-agent/impl.ts +136 -0
- package/src/modules/ssh-agent/ssh-agent/interface.ts +9 -0
- package/src/scripts/build.ts +559 -0
- package/src/scripts/check-npm-version.ts +7 -0
- package/src/scripts/command-launcher.js.ejs +186 -0
- package/src/scripts/constants.ts +11 -0
- package/src/scripts/kill-existing-agent.ts +22 -0
- package/src/scripts/publish.ts +89 -0
- package/src/scripts/windows-bin-shims.md +1104 -0
- package/multi-call-binary +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { $ } from 'bun';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { Effect, Option } from 'effect';
|
|
4
|
+
import { makeCommand } from '@/common/command';
|
|
5
|
+
import { log } from '@/common/log';
|
|
6
|
+
import { acquireToken } from './npm-auth';
|
|
7
|
+
import { fetchPackageInfo } from './package-registry';
|
|
8
|
+
import { addTrustedPublisher, setPackageAccessPolicy } from './trusted-publishing';
|
|
9
|
+
import { checkGitHubAccess } from './git-remote';
|
|
10
|
+
import { workflowExists, createWorkflow } from './workflow';
|
|
11
|
+
import { validatePackageJson } from './package-json-validate';
|
|
12
|
+
|
|
13
|
+
enum SetupError {
|
|
14
|
+
NoGitRepo = 'NoGitRepo',
|
|
15
|
+
NoPackageJson = 'NoPackageJson',
|
|
16
|
+
InvalidPackageJson = 'InvalidPackageJson',
|
|
17
|
+
NpmNotFound = 'NpmNotFound',
|
|
18
|
+
GhNotFound = 'GhNotFound',
|
|
19
|
+
NotLoggedIn = 'NotLoggedIn',
|
|
20
|
+
NoGitRemote = 'NoGitRemote',
|
|
21
|
+
RepoNotFound = 'RepoNotFound',
|
|
22
|
+
NoWriteAccess = 'NoWriteAccess',
|
|
23
|
+
BuildFailed = 'BuildFailed',
|
|
24
|
+
PublishFailed = 'PublishFailed',
|
|
25
|
+
TrustedPublishingFailed = 'TrustedPublishingFailed',
|
|
26
|
+
AccessPolicyFailed = 'AccessPolicyFailed',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const setup = makeCommand('setup', 'configure a new npm package for publishing', () =>
|
|
30
|
+
Effect.gen(function* () {
|
|
31
|
+
const cwd = process.cwd();
|
|
32
|
+
|
|
33
|
+
// 1. npm
|
|
34
|
+
log('checking npm...');
|
|
35
|
+
const npmCheck = yield* Effect.promise(() => $`which npm`.quiet().nothrow());
|
|
36
|
+
if (npmCheck.exitCode !== 0)
|
|
37
|
+
yield* Effect.fail(`${SetupError.NpmNotFound}: "npm" not found in PATH — install Node.js from nodejs.org`);
|
|
38
|
+
const npmVersion = (yield* Effect.promise(() => $`npm --version`.quiet().nothrow())).stdout.toString().trim();
|
|
39
|
+
const [major, minor] = npmVersion.split('.').map(Number);
|
|
40
|
+
if (major < 11 || (major === 11 && minor < 10))
|
|
41
|
+
yield* Effect.fail(`${SetupError.NpmNotFound}: npm ${npmVersion} is too old — "npm trust" requires 11.10.0+ (run "npm install -g npm")`);
|
|
42
|
+
const { token: npmToken, user: npmUser } = yield* acquireToken().pipe(
|
|
43
|
+
Effect.mapError(e => `${SetupError.NotLoggedIn}: npm authentication failed: ${e}`)
|
|
44
|
+
);
|
|
45
|
+
log(` ✓ logged into npm as ${npmUser}`);
|
|
46
|
+
|
|
47
|
+
// 2. GitHub CLI
|
|
48
|
+
log('checking GitHub...');
|
|
49
|
+
const ghCheck = yield* Effect.promise(() => $`which gh`.quiet().nothrow());
|
|
50
|
+
if (ghCheck.exitCode !== 0)
|
|
51
|
+
yield* Effect.fail(`${SetupError.GhNotFound}: "gh" not found in PATH — install GitHub CLI from cli.github.com`);
|
|
52
|
+
let ghUser = yield* Effect.promise(() => $`gh api user --jq .login`.quiet().nothrow());
|
|
53
|
+
if (ghUser.exitCode !== 0) {
|
|
54
|
+
log(' not logged in — opening gh auth login...');
|
|
55
|
+
yield* Effect.promise(() => $`gh auth login`.nothrow());
|
|
56
|
+
ghUser = yield* Effect.promise(() => $`gh api user --jq .login`.quiet().nothrow());
|
|
57
|
+
}
|
|
58
|
+
if (ghUser.exitCode !== 0)
|
|
59
|
+
yield* Effect.fail(`${SetupError.NotLoggedIn}: not logged into GitHub — run "gh auth login"`);
|
|
60
|
+
log(` ✓ logged into github as ${ghUser.stdout.toString().trim()}`);
|
|
61
|
+
|
|
62
|
+
// 3. git repo root
|
|
63
|
+
if (!existsSync(`${cwd}/.git`))
|
|
64
|
+
yield* Effect.fail(`${SetupError.NoGitRepo}: current directory is not a git repository — run "git init" first`);
|
|
65
|
+
|
|
66
|
+
// 4. package.json — read and fatal-check
|
|
67
|
+
log('checking package.json...');
|
|
68
|
+
const pkgFile = Bun.file(`${cwd}/package.json`);
|
|
69
|
+
if (!(yield* Effect.promise(() => pkgFile.exists())))
|
|
70
|
+
yield* Effect.fail(`${SetupError.NoPackageJson}: no package.json found — run this command from a package directory`);
|
|
71
|
+
let pkg: any = yield* Effect.tryPromise({
|
|
72
|
+
try: () => pkgFile.json(),
|
|
73
|
+
catch: () => `${SetupError.InvalidPackageJson}: package.json contains invalid JSON — fix it and retry`,
|
|
74
|
+
});
|
|
75
|
+
const { name } = pkg;
|
|
76
|
+
if (!name)
|
|
77
|
+
yield* Effect.fail(`${SetupError.InvalidPackageJson}: package.json is missing a "name" field`);
|
|
78
|
+
log(` name: ${name}`);
|
|
79
|
+
|
|
80
|
+
const fatal = validatePackageJson(pkg).find(i => i.fatal);
|
|
81
|
+
if (fatal)
|
|
82
|
+
yield* Effect.fail(`${SetupError.InvalidPackageJson}: ${fatal.field}: ${fatal.issue}`);
|
|
83
|
+
|
|
84
|
+
// 5. git remote
|
|
85
|
+
log('checking git remote...');
|
|
86
|
+
const remoteOption = yield* Effect.promise(() => getRemoteUrl(cwd));
|
|
87
|
+
if (Option.isNone(remoteOption))
|
|
88
|
+
yield* Effect.fail(`${SetupError.NoGitRemote}: no git remote configured — run "git remote add origin <url>" first`);
|
|
89
|
+
const remote = (remoteOption as Option.Some<string>).value;
|
|
90
|
+
log(` remote: ${remote}`);
|
|
91
|
+
const accessOption = yield* Effect.promise(() => checkGitHubAccess(remote));
|
|
92
|
+
if (Option.isNone(accessOption))
|
|
93
|
+
yield* Effect.fail(`${SetupError.RepoNotFound}: could not reach GitHub repo at ${remote} — check "gh auth status"`);
|
|
94
|
+
const access = (accessOption as Option.Some<{ owner: string; repo: string; canPush: boolean }>).value;
|
|
95
|
+
if (!access.canPush)
|
|
96
|
+
yield* Effect.fail(`${SetupError.NoWriteAccess}: no push access to ${access.owner}/${access.repo} — you need write permissions`);
|
|
97
|
+
const repoHttpsUrl = `https://github.com/${access.owner}/${access.repo}`;
|
|
98
|
+
log(` ✓ ${access.owner}/${access.repo} (write access confirmed)`);
|
|
99
|
+
|
|
100
|
+
// 6. Sync package.json repository URL from git remote; report other non-fatal issues
|
|
101
|
+
const expectedRepoUrl = `git+${repoHttpsUrl}.git`;
|
|
102
|
+
const currentRepoUrl = typeof pkg.repository === 'object' ? pkg.repository?.url : pkg.repository;
|
|
103
|
+
if (currentRepoUrl !== expectedRepoUrl) {
|
|
104
|
+
log('syncing package.json repository URL...');
|
|
105
|
+
log(` was: ${currentRepoUrl ?? '(missing)'}`);
|
|
106
|
+
pkg = { ...pkg, repository: { type: 'git', url: expectedRepoUrl } };
|
|
107
|
+
yield* Effect.promise(() => Bun.write(`${cwd}/package.json`, JSON.stringify(pkg, null, 2) + '\n'));
|
|
108
|
+
log(` ✓ set to ${expectedRepoUrl}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const nonFatal = validatePackageJson(pkg).filter(i => !i.fatal);
|
|
112
|
+
if (nonFatal.length > 0) {
|
|
113
|
+
log('package.json issues (fix manually):');
|
|
114
|
+
for (const { field, issue } of nonFatal) log(` ${field}: ${issue}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 7. Publish workflow
|
|
118
|
+
log('checking publish workflow...');
|
|
119
|
+
if (!(yield* Effect.promise(() => workflowExists(cwd)))) {
|
|
120
|
+
yield* Effect.promise(() => createWorkflow(cwd));
|
|
121
|
+
log(' ✓ created .github/workflows/publish.yml');
|
|
122
|
+
} else {
|
|
123
|
+
log(' ✓ already exists');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 8. npm package — initial publish if not yet on registry
|
|
127
|
+
log('checking npm registry...');
|
|
128
|
+
const infoOption = yield* fetchPackageInfo(name).pipe(
|
|
129
|
+
Effect.mapError(e => `${SetupError.NpmNotFound}: npm registry lookup failed: ${e}`)
|
|
130
|
+
);
|
|
131
|
+
if (Option.isNone(infoOption)) {
|
|
132
|
+
log(' package not yet published — building and creating initial release...');
|
|
133
|
+
const buildResult = yield* Effect.promise(() => $`bun run build`.nothrow());
|
|
134
|
+
if (buildResult.exitCode !== 0)
|
|
135
|
+
yield* Effect.fail(`${SetupError.BuildFailed}: build failed: ${buildResult.stderr.toString().trim()}`);
|
|
136
|
+
const distPkgFile = Bun.file(`${cwd}/dist/package.json`);
|
|
137
|
+
if (!(yield* Effect.promise(() => distPkgFile.exists())))
|
|
138
|
+
yield* Effect.fail(`${SetupError.BuildFailed}: build succeeded but dist/package.json is missing — build script must produce it`);
|
|
139
|
+
yield* Effect.tryPromise({
|
|
140
|
+
try: () => distPkgFile.json(),
|
|
141
|
+
catch: () => `${SetupError.BuildFailed}: dist/package.json exists but contains invalid JSON — check your build script`,
|
|
142
|
+
});
|
|
143
|
+
yield* npmPublish(npmToken).pipe(
|
|
144
|
+
Effect.mapError(e => `${SetupError.PublishFailed}: ${e}`)
|
|
145
|
+
);
|
|
146
|
+
log(` ✓ published ${name}`);
|
|
147
|
+
} else {
|
|
148
|
+
const version = infoOption.value.latestVersion ?? '(unknown)';
|
|
149
|
+
log(` ✓ exists (latest v${version})`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 9. Trusted publishing
|
|
153
|
+
log('checking trusted publishing...');
|
|
154
|
+
const ownerRepo = `${access.owner}/${access.repo}`;
|
|
155
|
+
const trust = yield* addTrustedPublisher(name, ownerRepo, npmToken).pipe(
|
|
156
|
+
Effect.mapError(e => `${SetupError.TrustedPublishingFailed}: ${e}`)
|
|
157
|
+
);
|
|
158
|
+
log(trust.alreadyConfigured ? ' ✓ already configured' : ' ✓ configured');
|
|
159
|
+
|
|
160
|
+
// 10. Package access policy — require 2FA, disallow automation tokens
|
|
161
|
+
log('setting package access policy...');
|
|
162
|
+
yield* setPackageAccessPolicy(name, npmToken, trust.otp).pipe(
|
|
163
|
+
Effect.mapError(e => `${SetupError.AccessPolicyFailed}: ${e}`)
|
|
164
|
+
);
|
|
165
|
+
log(' ✓ configured');
|
|
166
|
+
})
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
async function getRemoteUrl(cwd: string): Promise<Option.Option<string>> {
|
|
170
|
+
const result = await $`git -C ${cwd} remote get-url origin`.quiet().nothrow();
|
|
171
|
+
if (result.exitCode !== 0) return Option.none();
|
|
172
|
+
return Option.some(result.stdout.toString().trim());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function npmPublish(token: string): Effect.Effect<void, string> {
|
|
176
|
+
return Effect.tryPromise({
|
|
177
|
+
try: async () => {
|
|
178
|
+
const proc = Bun.spawn(['npm', 'publish', 'dist/', '--access', 'public', '--no-provenance'], {
|
|
179
|
+
stdin: 'inherit',
|
|
180
|
+
stdout: 'inherit',
|
|
181
|
+
stderr: 'inherit',
|
|
182
|
+
env: { ...process.env, NODE_AUTH_TOKEN: token },
|
|
183
|
+
});
|
|
184
|
+
const exitCode = await proc.exited;
|
|
185
|
+
if (exitCode !== 0) throw new Error('npm publish failed — see output above');
|
|
186
|
+
},
|
|
187
|
+
catch: (e) => e instanceof Error ? e.message : String(e),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { $ } from 'bun';
|
|
2
|
+
import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { Effect, Option } from 'effect';
|
|
5
|
+
import { log } from '@/common/log';
|
|
6
|
+
|
|
7
|
+
const npmrcPath = `${homedir()}/.npmrc`;
|
|
8
|
+
const TOKEN_RE = /^\/\/registry\.npmjs\.org\/:_authToken=(\S+)$/m;
|
|
9
|
+
|
|
10
|
+
function readNpmrc(): string {
|
|
11
|
+
try { return readFileSync(npmrcPath, 'utf8'); } catch { return ''; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function stripTokenFromDisk(): void {
|
|
15
|
+
const content = readNpmrc();
|
|
16
|
+
const stripped = content.replace(TOKEN_RE, '').replace(/\n{3,}/g, '\n\n').trim();
|
|
17
|
+
if (!stripped) {
|
|
18
|
+
try { unlinkSync(npmrcPath); } catch {}
|
|
19
|
+
} else {
|
|
20
|
+
writeFileSync(npmrcPath, stripped + '\n');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function whoamiWithToken(token: string): Promise<Option.Option<string>> {
|
|
25
|
+
const result = await $`npm whoami`
|
|
26
|
+
.env({ ...process.env, NODE_AUTH_TOKEN: token })
|
|
27
|
+
.quiet().nothrow();
|
|
28
|
+
if (result.exitCode !== 0) return Option.none();
|
|
29
|
+
return Option.some(result.stdout.toString().trim());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function acquireToken(): Effect.Effect<{ token: string; user: string }, string> {
|
|
33
|
+
return Effect.tryPromise({
|
|
34
|
+
try: async () => {
|
|
35
|
+
const existing = readNpmrc().match(TOKEN_RE)?.[1];
|
|
36
|
+
if (existing) {
|
|
37
|
+
const userOption = await whoamiWithToken(existing);
|
|
38
|
+
if (Option.isSome(userOption)) {
|
|
39
|
+
stripTokenFromDisk();
|
|
40
|
+
return { token: existing, user: userOption.value };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
log(' not logged in — opening npm login...');
|
|
45
|
+
await $`npm login`.nothrow();
|
|
46
|
+
|
|
47
|
+
const token = readNpmrc().match(TOKEN_RE)?.[1];
|
|
48
|
+
if (!token) throw new Error('npm login did not produce a token — try running "npm login" manually');
|
|
49
|
+
|
|
50
|
+
const userOption = await whoamiWithToken(token);
|
|
51
|
+
if (Option.isNone(userOption)) throw new Error('npm login succeeded but token verification failed');
|
|
52
|
+
|
|
53
|
+
stripTokenFromDisk();
|
|
54
|
+
return { token, user: userOption.value };
|
|
55
|
+
},
|
|
56
|
+
catch: (e) => e instanceof Error ? e.message : String(e),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
interface Pkg {
|
|
2
|
+
name: string;
|
|
3
|
+
version: string;
|
|
4
|
+
repository?: { type?: string; url?: string } | string;
|
|
5
|
+
publishConfig?: { access?: string; provenance?: boolean };
|
|
6
|
+
scripts?: Record<string, string>;
|
|
7
|
+
files?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PackageJsonIssue {
|
|
11
|
+
field: string;
|
|
12
|
+
issue: string;
|
|
13
|
+
fatal?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const GITHUB_URL_RE = /^git\+https:\/\/github\.com\/[^/]+\/[^/]+\.git$/;
|
|
17
|
+
|
|
18
|
+
function isValidGitHubUrl(url: string): boolean {
|
|
19
|
+
return GITHUB_URL_RE.test(url);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeRepoUrl(url: string): string {
|
|
23
|
+
if (!url.startsWith('git+')) url = `git+${url}`;
|
|
24
|
+
if (!url.endsWith('.git')) url = `${url}.git`;
|
|
25
|
+
return url;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function validatePackageJson(pkg: Pkg): PackageJsonIssue[] {
|
|
29
|
+
const issues: PackageJsonIssue[] = [];
|
|
30
|
+
|
|
31
|
+
const repoUrl = typeof pkg.repository === 'object' ? pkg.repository?.url : pkg.repository;
|
|
32
|
+
if (!repoUrl) {
|
|
33
|
+
issues.push({ field: 'repository', issue: 'missing — required for provenance' });
|
|
34
|
+
} else if (!isValidGitHubUrl(repoUrl)) {
|
|
35
|
+
issues.push({ field: 'repository.url', issue: `not a valid GitHub URL (got "${repoUrl}")` });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (pkg.publishConfig?.access !== 'public') {
|
|
39
|
+
issues.push({ field: 'publishConfig.access', issue: 'should be "public"' });
|
|
40
|
+
}
|
|
41
|
+
if (pkg.publishConfig?.provenance !== true) {
|
|
42
|
+
issues.push({ field: 'publishConfig.provenance', issue: 'should be true' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!pkg.version) {
|
|
46
|
+
issues.push({ field: 'version', issue: 'missing — add a "version" field (e.g. "0.0.1")', fatal: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!pkg.scripts?.['build']) {
|
|
50
|
+
issues.push({ field: 'scripts.build', issue: 'missing — add a "build" script before running setup', fatal: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!pkg.files?.includes('dist')) {
|
|
54
|
+
issues.push({ field: 'files', issue: '"dist" not included — built output won\'t be published' });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return issues;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildFixedPackageJson(pkg: Pkg, repoUrl: string): Pkg {
|
|
61
|
+
const url = normalizeRepoUrl(repoUrl);
|
|
62
|
+
return {
|
|
63
|
+
...pkg,
|
|
64
|
+
repository: { type: 'git', url },
|
|
65
|
+
publishConfig: { access: 'public', provenance: true },
|
|
66
|
+
files: pkg.files?.includes('dist') ? pkg.files : [...(pkg.files ?? []), 'dist'],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Effect, Option } from 'effect';
|
|
2
|
+
|
|
3
|
+
export interface PackageRegistryInfo {
|
|
4
|
+
name: string;
|
|
5
|
+
latestVersion: string;
|
|
6
|
+
distTags: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function fetchPackageInfo(name: string): Effect.Effect<Option.Option<PackageRegistryInfo>, string> {
|
|
10
|
+
return Effect.tryPromise({
|
|
11
|
+
try: async () => {
|
|
12
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}`);
|
|
13
|
+
if (res.status === 404) return Option.none<PackageRegistryInfo>();
|
|
14
|
+
if (!res.ok) throw new Error(`registry error ${res.status} for ${name}`);
|
|
15
|
+
const data = await res.json() as any;
|
|
16
|
+
return Option.some({
|
|
17
|
+
name: data.name,
|
|
18
|
+
latestVersion: data['dist-tags']?.latest,
|
|
19
|
+
distTags: data['dist-tags'] ?? {},
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
catch: (e) => e instanceof Error ? e.message : String(e),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
workflow_dispatch: {}
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment: prod
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
id-token: write
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- uses: oven-sh/setup-bun@v2
|
|
21
|
+
|
|
22
|
+
- name: Setup Node
|
|
23
|
+
uses: actions/setup-node@v4
|
|
24
|
+
with:
|
|
25
|
+
node-version: "24"
|
|
26
|
+
registry-url: 'https://registry.npmjs.org'
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: bun install
|
|
30
|
+
|
|
31
|
+
- name: Build
|
|
32
|
+
run: bun run build
|
|
33
|
+
|
|
34
|
+
- name: Publish
|
|
35
|
+
run: npm publish dist/ --access public
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { validatePackageJson } from './package-json-validate';
|
|
6
|
+
import { parseGithubOwnerRepo } from './git-remote';
|
|
7
|
+
import { workflowExists, createWorkflow } from './workflow';
|
|
8
|
+
import { Option } from "effect";
|
|
9
|
+
|
|
10
|
+
// ── validatePackageJson ──────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
describe('validatePackageJson', () => {
|
|
13
|
+
const base = {
|
|
14
|
+
name: '@scope/pkg',
|
|
15
|
+
version: '1.0.0',
|
|
16
|
+
repository: { type: 'git', url: 'git+https://github.com/owner/repo.git' },
|
|
17
|
+
publishConfig: { access: 'public' as const, provenance: true },
|
|
18
|
+
scripts: { build: 'bun run build' },
|
|
19
|
+
files: ['dist'],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
test('returns no issues for a valid package', () => {
|
|
23
|
+
expect(validatePackageJson(base)).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('fatal: missing version', () => {
|
|
27
|
+
const issues = validatePackageJson({ ...base, version: '' });
|
|
28
|
+
expect(issues.some(i => i.field === 'version' && i.fatal)).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('fatal: missing build script', () => {
|
|
32
|
+
const issues = validatePackageJson({ ...base, scripts: {} });
|
|
33
|
+
expect(issues.some(i => i.field === 'scripts.build' && i.fatal)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('non-fatal: missing repository', () => {
|
|
37
|
+
const issues = validatePackageJson({ ...base, repository: undefined as any });
|
|
38
|
+
expect(issues.some(i => i.field === 'repository' && !i.fatal)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('non-fatal: invalid repository url', () => {
|
|
42
|
+
const issues = validatePackageJson({ ...base, repository: { type: 'git', url: 'git+.git' } });
|
|
43
|
+
expect(issues.some(i => i.field === 'repository.url' && !i.fatal)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('non-fatal: publishConfig.access not public', () => {
|
|
47
|
+
const issues = validatePackageJson({ ...base, publishConfig: { access: 'restricted', provenance: true } });
|
|
48
|
+
expect(issues.some(i => i.field === 'publishConfig.access')).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('non-fatal: publishConfig.provenance not true', () => {
|
|
52
|
+
const issues = validatePackageJson({ ...base, publishConfig: { access: 'public', provenance: false } });
|
|
53
|
+
expect(issues.some(i => i.field === 'publishConfig.provenance')).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('non-fatal: dist not in files', () => {
|
|
57
|
+
const issues = validatePackageJson({ ...base, files: ['src'] });
|
|
58
|
+
expect(issues.some(i => i.field === 'files')).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ── parseGithubOwnerRepo ─────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
describe('parseGithubOwnerRepo', () => {
|
|
65
|
+
test('parses https url', () => {
|
|
66
|
+
expect(parseGithubOwnerRepo('https://github.com/owner/repo.git')).toEqual(Option.some({ owner: 'owner', repo: 'repo' }));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('parses https url without .git', () => {
|
|
70
|
+
expect(parseGithubOwnerRepo('https://github.com/owner/repo')).toEqual(Option.some({ owner: 'owner', repo: 'repo' }));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('parses standard ssh url', () => {
|
|
74
|
+
expect(parseGithubOwnerRepo('git@github.com:owner/repo.git')).toEqual(Option.some({ owner: 'owner', repo: 'repo' }));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('parses ssh alias url', () => {
|
|
78
|
+
expect(parseGithubOwnerRepo('git@github-ozyman42:ozyman42/test.git')).toEqual(Option.some({ owner: 'ozyman42', repo: 'test' }));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('returns null for unrecognised url', () => {
|
|
82
|
+
expect(parseGithubOwnerRepo('not-a-url')).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ── workflow helpers ─────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
describe('workflow', () => {
|
|
89
|
+
let tmpDir: string;
|
|
90
|
+
|
|
91
|
+
beforeEach(async () => {
|
|
92
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ozy-test-'));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterEach(async () => {
|
|
96
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('workflowExists returns false when missing', async () => {
|
|
100
|
+
expect(await workflowExists(tmpDir)).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('createWorkflow creates a valid yaml file', async () => {
|
|
104
|
+
await createWorkflow(tmpDir);
|
|
105
|
+
expect(await workflowExists(tmpDir)).toBe(true);
|
|
106
|
+
const content = await fs.readFile(path.join(tmpDir, '.github/workflows/publish.yml'), 'utf8');
|
|
107
|
+
expect(content).toContain('npm publish dist/');
|
|
108
|
+
expect(content).toContain('id-token: write');
|
|
109
|
+
expect(content).toContain('bun run build');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('createWorkflow is idempotent', async () => {
|
|
113
|
+
await createWorkflow(tmpDir);
|
|
114
|
+
await createWorkflow(tmpDir);
|
|
115
|
+
expect(await workflowExists(tmpDir)).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { $ } from 'bun';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { Effect } from 'effect';
|
|
4
|
+
import { log } from '@/common/log';
|
|
5
|
+
|
|
6
|
+
function encodePackageName(name: string): string {
|
|
7
|
+
if (!name.startsWith('@')) return encodeURIComponent(name);
|
|
8
|
+
const slash = name.indexOf('/');
|
|
9
|
+
return `@${name.slice(1, slash)}%2F${name.slice(slash + 1)}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function promptPasscode(): Promise<string> {
|
|
13
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
14
|
+
return new Promise(resolve => {
|
|
15
|
+
rl.question(' enter the passcode from the browser: ', answer => {
|
|
16
|
+
rl.close();
|
|
17
|
+
resolve(answer.trim());
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function registryPost(
|
|
23
|
+
url: string,
|
|
24
|
+
body: string,
|
|
25
|
+
authToken: string,
|
|
26
|
+
{ existingOtp, alreadyExistsStatus }: { existingOtp?: string; alreadyExistsStatus?: number } = {},
|
|
27
|
+
): Promise<{ otp?: string; alreadyExists: boolean }> {
|
|
28
|
+
const headers: Record<string, string> = {
|
|
29
|
+
Authorization: `Bearer ${authToken}`,
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const post = (otp?: string) => fetch(url, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: otp ? { ...headers, 'npm-otp': otp } : headers,
|
|
36
|
+
body,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const check = (res: Response) => {
|
|
40
|
+
if (res.ok) return true;
|
|
41
|
+
if (alreadyExistsStatus && res.status === alreadyExistsStatus) return true;
|
|
42
|
+
return false;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (existingOtp) {
|
|
46
|
+
const res = await post(existingOtp);
|
|
47
|
+
if (check(res)) return { otp: existingOtp, alreadyExists: !res.ok };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let res = await post();
|
|
51
|
+
if (check(res)) return { otp: undefined, alreadyExists: !res.ok };
|
|
52
|
+
|
|
53
|
+
if (res.status === 401) {
|
|
54
|
+
const notice = res.headers.get('npm-notice') ?? '';
|
|
55
|
+
const loginUrl = notice.match(/https:\/\/www\.npmjs\.com\/login\/[a-f0-9-]+/)?.[0];
|
|
56
|
+
if (!loginUrl) throw new Error('2FA required but could not parse login URL');
|
|
57
|
+
await $`open ${loginUrl}`.quiet().nothrow();
|
|
58
|
+
const otp = await promptPasscode();
|
|
59
|
+
res = await post(otp);
|
|
60
|
+
if (check(res)) return { otp, alreadyExists: !res.ok };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw new Error(`${res.status} ${await res.text()}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function addTrustedPublisher(
|
|
67
|
+
name: string,
|
|
68
|
+
ownerRepo: string,
|
|
69
|
+
token: string,
|
|
70
|
+
): Effect.Effect<{ alreadyConfigured: boolean; otp?: string }, string> {
|
|
71
|
+
const encoded = encodePackageName(name);
|
|
72
|
+
log(' setting up (may require 2FA)...');
|
|
73
|
+
const body = JSON.stringify([{
|
|
74
|
+
type: 'github',
|
|
75
|
+
claims: { repository: ownerRepo, workflow_ref: { file: 'pipeline.yml' } },
|
|
76
|
+
permissions: ['createPackage'],
|
|
77
|
+
}]);
|
|
78
|
+
return Effect.tryPromise({
|
|
79
|
+
try: async () => {
|
|
80
|
+
const { otp, alreadyExists } = await registryPost(
|
|
81
|
+
`https://registry.npmjs.org/-/package/${encoded}/trust`, body, token,
|
|
82
|
+
{ alreadyExistsStatus: 409 },
|
|
83
|
+
);
|
|
84
|
+
return { alreadyConfigured: alreadyExists, otp };
|
|
85
|
+
},
|
|
86
|
+
catch: (e) => `trust setup failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function setPackageAccessPolicy(
|
|
91
|
+
name: string,
|
|
92
|
+
token: string,
|
|
93
|
+
existingOtp?: string,
|
|
94
|
+
): Effect.Effect<void, string> {
|
|
95
|
+
const encoded = encodePackageName(name);
|
|
96
|
+
const body = JSON.stringify({ publish_requires_tfa: true, automation_token_overrides_tfa: false });
|
|
97
|
+
return Effect.tryPromise({
|
|
98
|
+
try: async () => {
|
|
99
|
+
await registryPost(
|
|
100
|
+
`https://registry.npmjs.org/-/package/${encoded}/access`, body, token,
|
|
101
|
+
{ existingOtp },
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
catch: (e) => `access policy setup failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import WORKFLOW_TEMPLATE from './publish-workflow.yml' with { type: 'text' };
|
|
4
|
+
|
|
5
|
+
const WORKFLOW_PATH = '.github/workflows/publish.yml';
|
|
6
|
+
|
|
7
|
+
export async function workflowExists(repoRoot: string): Promise<boolean> {
|
|
8
|
+
try {
|
|
9
|
+
await fs.access(path.join(repoRoot, WORKFLOW_PATH));
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function createWorkflow(repoRoot: string): Promise<void> {
|
|
17
|
+
const fullPath = path.join(repoRoot, WORKFLOW_PATH);
|
|
18
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
19
|
+
await fs.writeFile(fullPath, WORKFLOW_TEMPLATE, 'utf8');
|
|
20
|
+
}
|