@ozyman42/ozy-cli 0.4.8-windows-x64.0 → 0.4.8

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 (207) hide show
  1. package/bin/ozy +191 -0
  2. package/bin/ozy-signing-agent +191 -0
  3. package/bin/ozy-ssh-keygen +191 -0
  4. package/bin/ozy-virtual-security-key +191 -0
  5. package/dist/commands/git/hosts.js +18 -0
  6. package/dist/commands/git/hosts.js.map +1 -0
  7. package/dist/commands/git/index.js +8 -0
  8. package/dist/commands/git/index.js.map +1 -0
  9. package/dist/commands/git/setup.js +145 -0
  10. package/dist/commands/git/setup.js.map +1 -0
  11. package/dist/commands/index.js +13 -0
  12. package/dist/commands/index.js.map +1 -0
  13. package/dist/commands/npm/index.js +7 -0
  14. package/dist/commands/npm/index.js.map +1 -0
  15. package/dist/commands/npm/setup/git-remote.js +66 -0
  16. package/dist/commands/npm/setup/git-remote.js.map +1 -0
  17. package/dist/commands/npm/setup/index.js +168 -0
  18. package/dist/commands/npm/setup/index.js.map +1 -0
  19. package/dist/commands/npm/setup/npm-auth.js +62 -0
  20. package/dist/commands/npm/setup/npm-auth.js.map +1 -0
  21. package/dist/commands/npm/setup/package-json-validate.js +47 -0
  22. package/dist/commands/npm/setup/package-json-validate.js.map +1 -0
  23. package/dist/commands/npm/setup/package-registry.js +20 -0
  24. package/dist/commands/npm/setup/package-registry.js.map +1 -0
  25. package/dist/commands/npm/setup/setup.test.js +95 -0
  26. package/dist/commands/npm/setup/setup.test.js.map +1 -0
  27. package/dist/commands/npm/setup/trusted-publishing.js +84 -0
  28. package/dist/commands/npm/setup/trusted-publishing.js.map +1 -0
  29. package/dist/commands/npm/setup/workflow.js +19 -0
  30. package/dist/commands/npm/setup/workflow.js.map +1 -0
  31. package/dist/commands/ssh/get-sk-credential.js +104 -0
  32. package/dist/commands/ssh/get-sk-credential.js.map +1 -0
  33. package/dist/commands/ssh/index.js +7 -0
  34. package/dist/commands/ssh/index.js.map +1 -0
  35. package/dist/commands/upgrade.js +199 -0
  36. package/dist/commands/upgrade.js.map +1 -0
  37. package/dist/common/command.js +15 -0
  38. package/dist/common/command.js.map +1 -0
  39. package/dist/common/constants.js +25 -0
  40. package/dist/common/constants.js.map +1 -0
  41. package/dist/common/effective-modules-extensions.js +13 -0
  42. package/dist/common/effective-modules-extensions.js.map +1 -0
  43. package/dist/common/log.js +4 -0
  44. package/dist/common/log.js.map +1 -0
  45. package/dist/common/render-caller-tree.js +20 -0
  46. package/dist/common/render-caller-tree.js.map +1 -0
  47. package/dist/entrypoints/ozy-signing-agent.js +48 -0
  48. package/dist/entrypoints/ozy-signing-agent.js.map +1 -0
  49. package/dist/entrypoints/ozy-ssh-keygen.js +9 -0
  50. package/dist/entrypoints/ozy-ssh-keygen.js.map +1 -0
  51. package/dist/entrypoints/ozy-virtual-security-key.js +18 -0
  52. package/dist/entrypoints/ozy-virtual-security-key.js.map +1 -0
  53. package/dist/entrypoints/ozy.js +3 -0
  54. package/dist/entrypoints/ozy.js.map +1 -0
  55. package/dist/index.js +2 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/internal/thing.js +4 -0
  58. package/dist/internal/thing.js.map +1 -0
  59. package/dist/modules/cli/agent-client/impl.js +109 -0
  60. package/dist/modules/cli/agent-client/impl.js.map +1 -0
  61. package/dist/modules/cli/agent-client/interface.js +11 -0
  62. package/dist/modules/cli/agent-client/interface.js.map +1 -0
  63. package/dist/modules/cli/git/impl.js +124 -0
  64. package/dist/modules/cli/git/impl.js.map +1 -0
  65. package/dist/modules/cli/git/interface.js +3 -0
  66. package/dist/modules/cli/git/interface.js.map +1 -0
  67. package/dist/modules/cli/git/submodule/fix.js +257 -0
  68. package/dist/modules/cli/git/submodule/fix.js.map +1 -0
  69. package/dist/modules/cli/git/submodule/git-state.js +154 -0
  70. package/dist/modules/cli/git/submodule/git-state.js.map +1 -0
  71. package/dist/modules/cli/git/submodule/gitmodules.js +113 -0
  72. package/dist/modules/cli/git/submodule/gitmodules.js.map +1 -0
  73. package/dist/modules/cli/git/submodule/printer.js +232 -0
  74. package/dist/modules/cli/git/submodule/printer.js.map +1 -0
  75. package/dist/modules/cli/git/submodule/resolve-git-dir.js +16 -0
  76. package/dist/modules/cli/git/submodule/resolve-git-dir.js.map +1 -0
  77. package/dist/modules/cli/github/impl.js +137 -0
  78. package/dist/modules/cli/github/impl.js.map +1 -0
  79. package/dist/modules/cli/github/interface.js +2 -0
  80. package/dist/modules/cli/github/interface.js.map +1 -0
  81. package/dist/modules/cli/index.js +9 -0
  82. package/dist/modules/cli/index.js.map +1 -0
  83. package/dist/modules/common/crypto/impl.js +133 -0
  84. package/dist/modules/common/crypto/impl.js.map +1 -0
  85. package/dist/modules/common/crypto/interface.js +2 -0
  86. package/dist/modules/common/crypto/interface.js.map +1 -0
  87. package/dist/modules/common/index.js +10 -0
  88. package/dist/modules/common/index.js.map +1 -0
  89. package/dist/modules/common/kep-map-store/impl.js +42 -0
  90. package/dist/modules/common/kep-map-store/impl.js.map +1 -0
  91. package/dist/modules/common/kep-map-store/interface.js +2 -0
  92. package/dist/modules/common/kep-map-store/interface.js.map +1 -0
  93. package/dist/modules/common/os-platform/caller-info.js +132 -0
  94. package/dist/modules/common/os-platform/caller-info.js.map +1 -0
  95. package/dist/modules/common/os-platform/impl.js +95 -0
  96. package/dist/modules/common/os-platform/impl.js.map +1 -0
  97. package/dist/modules/common/os-platform/interface.js +2 -0
  98. package/dist/modules/common/os-platform/interface.js.map +1 -0
  99. package/dist/modules/common/os-platform/virtual-hid/index.js +12 -0
  100. package/dist/modules/common/os-platform/virtual-hid/index.js.map +1 -0
  101. package/dist/modules/common/os-platform/virtual-hid/linux.js +5 -0
  102. package/dist/modules/common/os-platform/virtual-hid/linux.js.map +1 -0
  103. package/dist/modules/common/os-platform/virtual-hid/mac.js +119 -0
  104. package/dist/modules/common/os-platform/virtual-hid/mac.js.map +1 -0
  105. package/dist/modules/common/os-platform/virtual-hid/windows.js +5 -0
  106. package/dist/modules/common/os-platform/virtual-hid/windows.js.map +1 -0
  107. package/dist/modules/common/ssh-config/impl.js +116 -0
  108. package/dist/modules/common/ssh-config/impl.js.map +1 -0
  109. package/dist/modules/common/ssh-config/interface.js +10 -0
  110. package/dist/modules/common/ssh-config/interface.js.map +1 -0
  111. package/dist/modules/ssh-agent/index.js +8 -0
  112. package/dist/modules/ssh-agent/index.js.map +1 -0
  113. package/dist/modules/ssh-agent/session/impl.js +265 -0
  114. package/dist/modules/ssh-agent/session/impl.js.map +1 -0
  115. package/dist/modules/ssh-agent/session/interface-rpc.js +20 -0
  116. package/dist/modules/ssh-agent/session/interface-rpc.js.map +1 -0
  117. package/dist/modules/ssh-agent/session/interface.js +32 -0
  118. package/dist/modules/ssh-agent/session/interface.js.map +1 -0
  119. package/dist/modules/ssh-agent/session/passkey-prf-page.js +204 -0
  120. package/dist/modules/ssh-agent/session/passkey-prf-page.js.map +1 -0
  121. package/dist/modules/ssh-agent/session/prf-flow.js +103 -0
  122. package/dist/modules/ssh-agent/session/prf-flow.js.map +1 -0
  123. package/dist/modules/ssh-agent/ssh-agent/impl.js +117 -0
  124. package/dist/modules/ssh-agent/ssh-agent/impl.js.map +1 -0
  125. package/dist/modules/ssh-agent/ssh-agent/interface.js +2 -0
  126. package/dist/modules/ssh-agent/ssh-agent/interface.js.map +1 -0
  127. package/dist/scripts/build.js +410 -0
  128. package/dist/scripts/build.js.map +1 -0
  129. package/dist/scripts/check-npm-version.js +8 -0
  130. package/dist/scripts/check-npm-version.js.map +1 -0
  131. package/dist/scripts/kill-existing-agent.js +21 -0
  132. package/dist/scripts/kill-existing-agent.js.map +1 -0
  133. package/dist/scripts/publish.js +46 -0
  134. package/dist/scripts/publish.js.map +1 -0
  135. package/package.json +52 -11
  136. package/src/commands/git/hosts.ts +20 -0
  137. package/src/commands/git/index.ts +9 -0
  138. package/src/commands/git/setup.ts +182 -0
  139. package/src/commands/index.ts +15 -0
  140. package/src/commands/npm/index.ts +8 -0
  141. package/src/commands/npm/setup/git-remote.ts +78 -0
  142. package/src/commands/npm/setup/index.ts +189 -0
  143. package/src/commands/npm/setup/npm-auth.ts +58 -0
  144. package/src/commands/npm/setup/package-json-validate.ts +68 -0
  145. package/src/commands/npm/setup/package-registry.ts +24 -0
  146. package/src/commands/npm/setup/publish-workflow.yml +35 -0
  147. package/src/commands/npm/setup/setup.test.ts +117 -0
  148. package/src/commands/npm/setup/trusted-publishing.ts +106 -0
  149. package/src/commands/npm/setup/workflow.ts +20 -0
  150. package/src/commands/ssh/get-sk-credential.ts +110 -0
  151. package/src/commands/ssh/index.ts +8 -0
  152. package/src/commands/upgrade.ts +220 -0
  153. package/src/common/command.ts +16 -0
  154. package/src/common/constants.ts +26 -0
  155. package/src/common/effective-modules-extensions.ts +24 -0
  156. package/src/common/log.ts +3 -0
  157. package/src/common/render-caller-tree.ts +22 -0
  158. package/src/entrypoints/ozy-signing-agent.ts +48 -0
  159. package/src/entrypoints/ozy-ssh-keygen.ts +10 -0
  160. package/src/entrypoints/ozy-virtual-security-key.ts +21 -0
  161. package/src/entrypoints/ozy.ts +2 -0
  162. package/src/index.ts +1 -0
  163. package/src/internal/thing.ts +3 -0
  164. package/src/modules/cli/agent-client/impl.ts +152 -0
  165. package/src/modules/cli/agent-client/interface.ts +17 -0
  166. package/src/modules/cli/git/impl.ts +133 -0
  167. package/src/modules/cli/git/interface.ts +23 -0
  168. package/src/modules/cli/git/submodule/fix.ts +298 -0
  169. package/src/modules/cli/git/submodule/git-state.ts +217 -0
  170. package/src/modules/cli/git/submodule/gitmodules.ts +126 -0
  171. package/src/modules/cli/git/submodule/printer.ts +256 -0
  172. package/src/modules/cli/git/submodule/resolve-git-dir.ts +14 -0
  173. package/src/modules/cli/github/impl.ts +169 -0
  174. package/src/modules/cli/github/interface.ts +29 -0
  175. package/src/modules/cli/index.ts +16 -0
  176. package/src/modules/common/crypto/impl.ts +173 -0
  177. package/src/modules/common/crypto/interface.ts +20 -0
  178. package/src/modules/common/index.ts +19 -0
  179. package/src/modules/common/kep-map-store/impl.ts +47 -0
  180. package/src/modules/common/kep-map-store/interface.ts +9 -0
  181. package/src/modules/common/os-platform/caller-info.ts +152 -0
  182. package/src/modules/common/os-platform/impl.ts +91 -0
  183. package/src/modules/common/os-platform/interface.ts +16 -0
  184. package/src/modules/common/os-platform/virtual-hid/index.ts +12 -0
  185. package/src/modules/common/os-platform/virtual-hid/linux.ts +7 -0
  186. package/src/modules/common/os-platform/virtual-hid/mac.ts +150 -0
  187. package/src/modules/common/os-platform/virtual-hid/windows.ts +7 -0
  188. package/src/modules/common/ssh-config/impl.ts +130 -0
  189. package/src/modules/common/ssh-config/interface.ts +33 -0
  190. package/src/modules/ssh-agent/index.ts +13 -0
  191. package/src/modules/ssh-agent/session/agent-notes.md +210 -0
  192. package/src/modules/ssh-agent/session/design.md +2 -0
  193. package/src/modules/ssh-agent/session/html-refactor.md +23 -0
  194. package/src/modules/ssh-agent/session/impl.ts +328 -0
  195. package/src/modules/ssh-agent/session/interface-rpc.ts +24 -0
  196. package/src/modules/ssh-agent/session/interface.ts +62 -0
  197. package/src/modules/ssh-agent/session/passkey-prf-page.ts +224 -0
  198. package/src/modules/ssh-agent/session/prf-flow.ts +152 -0
  199. package/src/modules/ssh-agent/ssh-agent/impl.ts +136 -0
  200. package/src/modules/ssh-agent/ssh-agent/interface.ts +9 -0
  201. package/src/scripts/build.ts +504 -0
  202. package/src/scripts/check-npm-version.ts +7 -0
  203. package/src/scripts/command-launcher.js.ejs +168 -0
  204. package/src/scripts/kill-existing-agent.ts +22 -0
  205. package/src/scripts/publish.ts +47 -0
  206. package/src/scripts/windows-bin-shims.md +1104 -0
  207. package/multi-call-binary.exe +0 -0
@@ -0,0 +1,182 @@
1
+ import { Command } from 'commander';
2
+ import { Effect, Layer, Option, pipe } from 'effect';
3
+ import { log } from '@/common/log';
4
+ import { cliModules, type CLIModules } from '@/modules/cli';
5
+ import { commonModules, CommonModules } from '@/modules/common';
6
+ import { effunct, type EffectGen } from 'effective-modules';
7
+ import { GitImpl } from '@/modules/cli/git/impl';
8
+ import { GitHubImpl } from '@/modules/cli/github/impl';
9
+ import { AgentClientImpl } from '@/modules/cli/agent-client/impl';
10
+ import { OSPlatformImpl } from '@/modules/common/os-platform/impl';
11
+ import { SSHConfigImpl } from '@/modules/common/ssh-config/impl';
12
+ import { AGENT_SOCK_FILE_PATH, FUTURE_TOOL_NAME, STANDARD_REMOTE_PREFIX } from '../../common/constants';
13
+ import { CredentialId } from '@/modules/common/crypto/impl';
14
+ import { BunFileSystem } from "@effect/platform-bun";
15
+ import type { GitUser } from '@/modules/cli/git/interface';
16
+
17
+ type AllModules = CLIModules.Git | CLIModules.GitHub | CLIModules.AgentClient | CommonModules.SSHConfig;
18
+
19
+ const GITHUB_CLIENT_ID = 'Ov23liKYxk1Ag7SsNhbP';
20
+ const GITHUB_CLIENT_SECRET = 'e2901fbe93c591e7a53a903e70490ff87e998159';
21
+ const OAUTH_SCOPES = 'read:user user:email write:ssh_signing_key write:public_key';
22
+ const KEY_PREFIX = `${FUTURE_TOOL_NAME}-`;
23
+
24
+ function parseCredentialIdFromTitle(title: string): Option.Option<string> {
25
+ const match = title.match(/\(([^)]+)\)$/);
26
+ if (!match) return Option.none();
27
+ return Option.some(match[1]!);
28
+ }
29
+
30
+ function baseKey(pubkey: string): string {
31
+ return pubkey.split(' ').slice(0, 2).join(' ');
32
+ }
33
+
34
+ export enum GitSetupError {
35
+ NotInGitRepositoryError = 'NotInGitRepositoryError',
36
+ NoRemoteOriginError = 'NoRemoteOriginError',
37
+ OAuthFailedError = 'OAuthFailedError',
38
+ GitHubUserFetchFailedError = 'GitHubUserFetchFailedError',
39
+ RepoAccessDeniedError = 'RepoAccessDeniedError',
40
+ MultipleJiveSigningKeysError = 'MultipleJiveSigningKeysError',
41
+ MultipleJiveAuthnKeysError = 'MultipleJiveAuthnKeysError',
42
+ MissingCredentialIdError = 'MissingCredentialIdError',
43
+ SigningAuthnKeyMismatchError = 'SigningAuthnKeyMismatchError',
44
+ AgentStartFailedError = 'AgentStartFailedError',
45
+ AgentProcedureFailedError = 'AgentProcedureFailedError',
46
+ GitCloneFailedError = 'GitCloneFailedError',
47
+ GitConfigFailedError = 'GitConfigFailedError',
48
+ }
49
+
50
+ function* gitSetup(ownerRepoArg?: string): Effect.fn.Return<void, string, AllModules> {
51
+ const git = yield* cliModules.Git;
52
+ const github = yield* cliModules.GitHub;
53
+ const agentClient = yield* cliModules.AgentClient;
54
+ const sshConfig = yield* commonModules.SSHConfig;
55
+ const isCloneMode = ownerRepoArg !== undefined;
56
+
57
+ // 1. Parse owner/repo
58
+ const { owner, repo } = yield* git.resolveOwnerAndRepo(Option.fromNullishOr(ownerRepoArg));
59
+ log(`Setting up ${owner}/${repo}`);
60
+
61
+ // 2. GitHub OAuth
62
+ const token = yield* github.authorize(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, OAUTH_SCOPES);
63
+ log('✓ Authorized with GitHub');
64
+
65
+ // 3. Fetch user info
66
+ const { login, email } = yield* github.getUser(token);
67
+ const gitUser: GitUser = {name: login, email: email};
68
+ log(` Logged in as: ${gitUser.name} <${gitUser.email}>`);
69
+
70
+ // 4. Check GitHub signing keys for <tool name>: prefix
71
+ const signingKeys = yield* github.getSigningKeys(token);
72
+ const toolKeys = signingKeys.filter(k => k.title.startsWith(KEY_PREFIX));
73
+ if (toolKeys.length > 1)
74
+ return yield* Effect.fail(`${GitSetupError.MultipleJiveSigningKeysError}: Found ${toolKeys.length} signing keys with "${KEY_PREFIX}" prefix — expected at most 1`);
75
+ let maybeKey: Option.Option<{ credentialId: string; pubkey: string }> = Option.none();
76
+ if (toolKeys[0]) {
77
+ const credentialId = parseCredentialIdFromTitle(toolKeys[0].title);
78
+ if (Option.isNone(credentialId))
79
+ return yield* Effect.fail(`${GitSetupError.MissingCredentialIdError}: signing key title "${toolKeys[0].title}" has no credentialId`);
80
+ maybeKey = Option.some({ credentialId: credentialId.value, pubkey: baseKey(toolKeys[0].key) });
81
+ }
82
+
83
+ // 5. Check authn keys too — must match signing key if both present.
84
+ const authnKeys = yield* github.getAuthnKeys(token);
85
+ const toolAuthnKeys = authnKeys.filter(k => k.title.startsWith(KEY_PREFIX));
86
+ if (toolAuthnKeys.length > 1)
87
+ return yield* Effect.fail(`${GitSetupError.MultipleJiveAuthnKeysError}: Found ${toolAuthnKeys.length} authn keys with "${KEY_PREFIX}" prefix — expected at most 1`);
88
+ let maybeAuthnKey: Option.Option<{ credentialId: string; pubkey: string }> = Option.none();
89
+ if (toolAuthnKeys[0]) {
90
+ const credentialId = parseCredentialIdFromTitle(toolAuthnKeys[0].title);
91
+ if (Option.isNone(credentialId))
92
+ return yield* Effect.fail(`${GitSetupError.MissingCredentialIdError}: authn key title "${toolAuthnKeys[0].title}" has no credentialId`);
93
+ maybeAuthnKey = Option.some({ credentialId: credentialId.value, pubkey: baseKey(toolAuthnKeys[0].key) });
94
+ }
95
+ if (Option.isSome(maybeKey) && Option.isSome(maybeAuthnKey) && maybeKey.value.pubkey !== maybeAuthnKey.value.pubkey)
96
+ return yield* Effect.fail(`${GitSetupError.SigningAuthnKeyMismatchError}: signing key and authn key pubkeys don't match — GitHub account is in an inconsistent state`);
97
+
98
+ // 6. Call agent to get/create/verify key (starts agent if not running)
99
+ const { credentialId, pubkey } = yield* pipe(
100
+ Effect.gen(() => agentClient.usingClient(function* (client): EffectGen<{ pubkey: string; credentialId: string }, string> {
101
+ return yield* pipe(
102
+ client.Setup({
103
+ pubkey: Option.map(maybeKey, k => k.pubkey),
104
+ credentialId: Option.map(maybeKey, k => k.credentialId),
105
+ username: login,
106
+ }),
107
+ Effect.mapError(e => `${GitSetupError.AgentProcedureFailedError}: ${String(e)}`)
108
+ );
109
+ })),
110
+ Effect.catchTag("AgentClientError", err => Effect.fail(err.reason)),
111
+ Effect.catchTag("AgentClientUsageError", err => Effect.fail(err.cause))
112
+ );
113
+ const credentialFriendlyName = new CredentialId(credentialId).humanReadableName;
114
+ const keyTitle = `${credentialFriendlyName} (${credentialId})`;
115
+
116
+ // 8. Register signing key on GitHub if not already present
117
+ if (Option.isNone(maybeKey)) {
118
+ yield* github.addSigningKey(token, keyTitle, pubkey);
119
+ log(`✓ Registered GitHub signing key: ${keyTitle}`);
120
+ }
121
+
122
+ // 9. Register key as authn key on GitHub account if not already present
123
+ if (Option.isNone(maybeAuthnKey)) {
124
+ yield* github.addAuthnKey(token, keyTitle, pubkey);
125
+ log(`✓ Registered authn key on GitHub account`);
126
+ }
127
+
128
+ // 10. Write ssh config (pubkey file written by agent via KeyMapStore.addKey)
129
+ const pubkeyPath = sshConfig.getPubkeyPath(credentialFriendlyName);
130
+ yield* sshConfig.writeHost({
131
+ section: {
132
+ HostName: "github.com",
133
+ User: "git",
134
+ IdentityFile: pubkeyPath,
135
+ IdentitiesOnly: "yes",
136
+ IdentityAgent: AGENT_SOCK_FILE_PATH,
137
+ },
138
+ });
139
+
140
+ // 11. Clone if in clone mode
141
+ if (isCloneMode) {
142
+ yield* git.clone(owner, repo);
143
+ log(`✓ Cloned into ${repo}/`);
144
+ }
145
+
146
+ // 12. Set repo config plus any submodules' config
147
+ log('\nSetting git config:');
148
+ yield* git.setupRepo({
149
+ dir: process.cwd(),
150
+ remoteURL: `${STANDARD_REMOTE_PREFIX}${owner}/${repo}.git`,
151
+ user: gitUser,
152
+ pubkeyPath,
153
+ });
154
+
155
+ log(`\n✓ Setup complete for ${owner}/${repo}`);
156
+ }
157
+
158
+ const layerLive = pipe(
159
+ GitImpl.Layer,
160
+ Layer.provideMerge(GitHubImpl.Layer),
161
+ Layer.provideMerge(AgentClientImpl.Layer),
162
+ Layer.provideMerge(OSPlatformImpl.Layer),
163
+ Layer.provideMerge(SSHConfigImpl.Layer),
164
+ Layer.provideMerge(BunFileSystem.layer)
165
+ )
166
+
167
+ export const setup = new Command('setup')
168
+ .description('setup verified git commits for current repo')
169
+ .argument('[owner/repo]', 'GitHub repo to clone and set up (omit if already in a repo with a remote)')
170
+ .action(async (ownerRepo?: string) => {
171
+ function* program() {
172
+ yield* pipe(
173
+ effunct(gitSetup)(ownerRepo),
174
+ Effect.provide(layerLive),
175
+ Effect.catch(err => Effect.gen(function* () {
176
+ log(`✗ ${err}`);
177
+ }))
178
+ );
179
+ }
180
+ await Effect.runPromise(Effect.gen(program));
181
+ process.exit();
182
+ });
@@ -0,0 +1,15 @@
1
+ import { program } from 'commander';
2
+ import { git } from './git';
3
+ import { npm } from './npm';
4
+ import { upgrade } from './upgrade';
5
+ import { ssh } from "./ssh";
6
+ import { CLI_CMD_NAME, CURRENT_VERSION } from '@/common/constants';
7
+
8
+ program.name(CLI_CMD_NAME).version(CURRENT_VERSION);
9
+
10
+ [git, npm, ssh, upgrade]
11
+ .forEach(cmd => { program.addCommand(cmd) });
12
+
13
+ program.parse();
14
+
15
+ if (process.argv.length <= 2) program.help();
@@ -0,0 +1,8 @@
1
+ import { Command } from 'commander';
2
+ import { setup } from './setup';
3
+
4
+ export const npm = new Command('npm')
5
+ .summary('npm package management utilities');
6
+
7
+ [setup]
8
+ .forEach(cmd => { npm.addCommand(cmd) });
@@ -0,0 +1,78 @@
1
+ import { $ } from 'bun';
2
+ import { Effect, Option } from 'effect';
3
+
4
+ export interface RepoAccess {
5
+ owner: string;
6
+ repo: string;
7
+ canPush: boolean;
8
+ }
9
+
10
+ export interface CreatedRepo {
11
+ owner: string;
12
+ repo: string;
13
+ url: string;
14
+ sshUrl: string;
15
+ }
16
+
17
+ // Handles https://github.com/owner/repo.git, git@github.com:owner/repo.git,
18
+ // and SSH aliases like github-ozyman42:owner/repo.git
19
+ export function parseGithubOwnerRepo(url: string): Option.Option<{ owner: string; repo: string }> {
20
+ const https = url.match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?$/);
21
+ if (https) return Option.some({ owner: https[1]!, repo: https[2]! });
22
+ const alias = url.match(/^[^:]+:([^/]+)\/([^/]+?)(?:\.git)?$/);
23
+ if (alias) return Option.some({ owner: alias[1]!, repo: alias[2]! });
24
+ return Option.none();
25
+ }
26
+
27
+ async function viewRepo(nameWithOwner: string): Promise<Option.Option<CreatedRepo>> {
28
+ const view = await $`gh repo view ${nameWithOwner} --json name,owner,url,sshUrl`.quiet().nothrow();
29
+ if (view.exitCode !== 0) return Option.none();
30
+ try {
31
+ const data = JSON.parse(view.stdout.toString());
32
+ return Option.some({ owner: data.owner.login, repo: data.name, url: data.url, sshUrl: data.sshUrl });
33
+ } catch {
34
+ return Option.none();
35
+ }
36
+ }
37
+
38
+ export function ensureGitHubRepo(name: string, isPrivate = false): Effect.Effect<{ repo: CreatedRepo; created: boolean }, string> {
39
+ return Effect.tryPromise({
40
+ try: async () => {
41
+ const whoami = await $`gh api user --jq .login`.quiet().nothrow();
42
+ const owner = whoami.exitCode === 0 ? whoami.stdout.toString().trim() : null;
43
+
44
+ if (owner) {
45
+ const existingOption = await viewRepo(`${owner}/${name}`);
46
+ if (Option.isSome(existingOption)) return { repo: existingOption.value, created: false };
47
+ }
48
+
49
+ const visibility = isPrivate ? '--private' : '--public';
50
+ const create = await $`gh repo create ${name} ${visibility}`.quiet().nothrow();
51
+ if (create.exitCode !== 0) {
52
+ throw new Error(`gh repo create failed: ${create.stderr.toString().trim()}`);
53
+ }
54
+ const createdOption = await viewRepo(name);
55
+ if (Option.isNone(createdOption)) throw new Error(`repo created but could not retrieve details for "${name}"`);
56
+ return { repo: createdOption.value, created: true };
57
+ },
58
+ catch: (e) => e instanceof Error ? e.message : String(e),
59
+ });
60
+ }
61
+
62
+ export async function checkGitHubAccess(remoteUrl: string): Promise<Option.Option<RepoAccess>> {
63
+ const parsedOption = parseGithubOwnerRepo(remoteUrl);
64
+ if (Option.isNone(parsedOption)) return Option.none();
65
+ const { owner, repo } = parsedOption.value;
66
+
67
+ const result = await $`gh api repos/${owner}/${repo}`.quiet().nothrow();
68
+ if (result.exitCode !== 0) return Option.none();
69
+
70
+ let data: any;
71
+ try {
72
+ data = JSON.parse(result.stdout.toString());
73
+ } catch {
74
+ return Option.none();
75
+ }
76
+
77
+ return Option.some({ owner, repo, canPush: data?.permissions?.push === true });
78
+ }
@@ -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