@stream44.studio/t44 0.4.0-rc.24
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/.dco-signatures +9 -0
- package/.github/workflows/dco.yaml +12 -0
- package/.github/workflows/gordian-open-integrity.yaml +13 -0
- package/.github/workflows/test.yaml +31 -0
- package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity.yaml +21 -0
- package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
- package/.o/stream44.studio/assets/Icon-v1.svg +1170 -0
- package/.repo-identifier +1 -0
- package/DCO.md +34 -0
- package/LICENSE.txt +186 -0
- package/README.md +189 -0
- package/bin/activate +36 -0
- package/bin/activate.ts +30 -0
- package/bin/postinstall.sh +19 -0
- package/bin/shell +27 -0
- package/bin/t44 +27 -0
- package/caps/ConfigSchemaStruct.ts +55 -0
- package/caps/Home.ts +57 -0
- package/caps/HomeRegistry.ts +319 -0
- package/caps/HomeRegistryFile.ts +144 -0
- package/caps/JsonSchemas.ts +220 -0
- package/caps/OpenApiSchema.ts +67 -0
- package/caps/PackageDescriptor.ts +88 -0
- package/caps/ProjectCatalogs.ts +153 -0
- package/caps/ProjectDeployment.ts +426 -0
- package/caps/ProjectDevelopment.ts +257 -0
- package/caps/ProjectPublishing.ts +654 -0
- package/caps/ProjectPulling.ts +234 -0
- package/caps/ProjectRack.ts +155 -0
- package/caps/ProjectRepository.ts +332 -0
- package/caps/ProjectTest.ts +251 -0
- package/caps/ProjectTestLib.ts +257 -0
- package/caps/RootKey.ts +219 -0
- package/caps/SigningKey.ts +243 -0
- package/caps/TaskWorkflow.ts +192 -0
- package/caps/WorkspaceCli.ts +448 -0
- package/caps/WorkspaceConfig.ts +268 -0
- package/caps/WorkspaceConfig.yaml +87 -0
- package/caps/WorkspaceConfigFile.ts +902 -0
- package/caps/WorkspaceConnection.ts +329 -0
- package/caps/WorkspaceEntityConfig.ts +78 -0
- package/caps/WorkspaceEntityConfig.v0.ts +77 -0
- package/caps/WorkspaceEntityFact.ts +218 -0
- package/caps/WorkspaceInfo.ts +619 -0
- package/caps/WorkspaceInit.ts +30 -0
- package/caps/WorkspaceKey.ts +338 -0
- package/caps/WorkspaceModel.ts +373 -0
- package/caps/WorkspaceProjects.ts +636 -0
- package/caps/WorkspacePrompt.ts +430 -0
- package/caps/WorkspaceShell.sh +39 -0
- package/caps/WorkspaceShell.ts +104 -0
- package/caps/WorkspaceShell.yaml +64 -0
- package/caps/WorkspaceShellCli.ts +109 -0
- package/caps/patterns/README.md +2 -0
- package/caps/patterns/git-scm.com/ProjectPublishing.ts +507 -0
- package/caps/patterns/semver.org/ProjectPublishing.ts +458 -0
- package/docs/Overview.drawio +248 -0
- package/docs/Overview.svg +4 -0
- package/examples/01-Lifecycle/main.test.ts +223 -0
- package/lib/crypto.ts +53 -0
- package/lib/key.ts +381 -0
- package/lib/schema-console-renderer.ts +181 -0
- package/lib/schema-resolver.ts +349 -0
- package/lib/ucan.ts +137 -0
- package/package.json +91 -0
- package/standalone-rt.test.ts +150 -0
- package/standalone-rt.ts +140 -0
- package/structs/HomeRegistry.ts +55 -0
- package/structs/HomeRegistryConfig.ts +60 -0
- package/structs/ProjectCatalogsConfig.ts +53 -0
- package/structs/ProjectDeploymentConfig.ts +56 -0
- package/structs/ProjectDeploymentFact.ts +106 -0
- package/structs/ProjectPublishingConfig.ts +78 -0
- package/structs/ProjectPublishingFact.ts +68 -0
- package/structs/ProjectPullingConfig.ts +52 -0
- package/structs/ProjectRack.ts +51 -0
- package/structs/ProjectRackConfig.ts +56 -0
- package/structs/RepositoryOriginDescriptor.ts +51 -0
- package/structs/RootKeyConfig.ts +64 -0
- package/structs/SigningKeyConfig.ts +64 -0
- package/structs/Workspace.ts +56 -0
- package/structs/WorkspaceCatalogs.ts +56 -0
- package/structs/WorkspaceCliConfig.ts +53 -0
- package/structs/WorkspaceConfig.ts +64 -0
- package/structs/WorkspaceConfigFile.ts +50 -0
- package/structs/WorkspaceConfigFileMeta.ts +70 -0
- package/structs/WorkspaceKey.ts +55 -0
- package/structs/WorkspaceKeyConfig.ts +56 -0
- package/structs/WorkspaceMappingsConfig.ts +56 -0
- package/structs/WorkspaceProject.ts +104 -0
- package/structs/WorkspaceProjectsConfig.ts +67 -0
- package/structs/WorkspaceShellConfig.ts +83 -0
- package/structs/patterns/README.md +2 -0
- package/structs/patterns/git-scm.com/ProjectPublishingFact.ts +46 -0
- package/tsconfig.json +33 -0
- package/workspace-rt.ts +152 -0
- package/workspace.yaml +3 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import * as path from 'path'
|
|
2
|
+
import * as fsPromises from 'fs/promises'
|
|
3
|
+
import { constants as fsConstants } from 'fs'
|
|
4
|
+
import { spawn } from 'bun'
|
|
5
|
+
|
|
6
|
+
export async function capsule({
|
|
7
|
+
encapsulate,
|
|
8
|
+
CapsulePropertyTypes,
|
|
9
|
+
makeImportStack
|
|
10
|
+
}: {
|
|
11
|
+
encapsulate: any
|
|
12
|
+
CapsulePropertyTypes: any
|
|
13
|
+
makeImportStack: any
|
|
14
|
+
}) {
|
|
15
|
+
return encapsulate({
|
|
16
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
17
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
18
|
+
'#': {
|
|
19
|
+
path: {
|
|
20
|
+
type: CapsulePropertyTypes.Constant,
|
|
21
|
+
value: path,
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
fs: {
|
|
25
|
+
type: CapsulePropertyTypes.Constant,
|
|
26
|
+
value: {
|
|
27
|
+
...fsPromises,
|
|
28
|
+
...fsConstants
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
spawnProcess: {
|
|
33
|
+
type: CapsulePropertyTypes.Function,
|
|
34
|
+
value: async function (this: any, options: any): Promise<any> {
|
|
35
|
+
const {
|
|
36
|
+
cmd,
|
|
37
|
+
cwd = process.cwd(),
|
|
38
|
+
waitForReady = false,
|
|
39
|
+
readySignal = 'READY',
|
|
40
|
+
waitForExit = false,
|
|
41
|
+
showOutput = false,
|
|
42
|
+
env = {},
|
|
43
|
+
verbose = false,
|
|
44
|
+
detached = false
|
|
45
|
+
} = options;
|
|
46
|
+
|
|
47
|
+
const outputData = { stdout: '', stderr: '' };
|
|
48
|
+
const mergedEnv = { ...process.env, ...env };
|
|
49
|
+
|
|
50
|
+
if (verbose && env.NODE_ENV) {
|
|
51
|
+
console.log('[spawnProcess] Setting NODE_ENV to:', env.NODE_ENV);
|
|
52
|
+
console.log('[spawnProcess] Merged env NODE_ENV:', mergedEnv.NODE_ENV);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const proc = spawn({
|
|
56
|
+
cmd,
|
|
57
|
+
cwd,
|
|
58
|
+
stdout: 'pipe',
|
|
59
|
+
stderr: 'pipe',
|
|
60
|
+
env: mergedEnv
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (detached && proc.unref) {
|
|
64
|
+
proc.unref();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let readySignalFound: (() => void) | null = null;
|
|
68
|
+
const readyPromise = waitForReady ? new Promise<void>((resolve) => {
|
|
69
|
+
readySignalFound = resolve;
|
|
70
|
+
}) : null;
|
|
71
|
+
|
|
72
|
+
if (proc.stdout && !detached) {
|
|
73
|
+
const reader = (proc.stdout as any).getReader();
|
|
74
|
+
const decoder = new TextDecoder();
|
|
75
|
+
(async () => {
|
|
76
|
+
try {
|
|
77
|
+
while (true) {
|
|
78
|
+
const { done, value } = await reader.read();
|
|
79
|
+
if (done) break;
|
|
80
|
+
const chunk = decoder.decode(value);
|
|
81
|
+
outputData.stdout += chunk;
|
|
82
|
+
if (waitForReady && readySignalFound && chunk.indexOf(readySignal) !== -1) {
|
|
83
|
+
readySignalFound();
|
|
84
|
+
readySignalFound = null;
|
|
85
|
+
}
|
|
86
|
+
if (showOutput || verbose) {
|
|
87
|
+
process.stdout.write(chunk);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
// Stream closed
|
|
92
|
+
}
|
|
93
|
+
})();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (proc.stderr && !detached) {
|
|
97
|
+
const reader = (proc.stderr as any).getReader();
|
|
98
|
+
const decoder = new TextDecoder();
|
|
99
|
+
(async () => {
|
|
100
|
+
try {
|
|
101
|
+
while (true) {
|
|
102
|
+
const { done, value } = await reader.read();
|
|
103
|
+
if (done) break;
|
|
104
|
+
const chunk = decoder.decode(value);
|
|
105
|
+
outputData.stderr += chunk;
|
|
106
|
+
if (waitForReady && readySignalFound && chunk.indexOf(readySignal) !== -1) {
|
|
107
|
+
readySignalFound();
|
|
108
|
+
readySignalFound = null;
|
|
109
|
+
}
|
|
110
|
+
if (showOutput || verbose) {
|
|
111
|
+
process.stderr.write(chunk);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch (e) {
|
|
115
|
+
// Stream closed
|
|
116
|
+
}
|
|
117
|
+
})();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (waitForReady && readyPromise) {
|
|
121
|
+
await Promise.race([
|
|
122
|
+
readyPromise,
|
|
123
|
+
proc.exited.then(() => {
|
|
124
|
+
throw new Error(`Process exited with code ${proc.exitCode} before emitting ${readySignal} signal. stderr: ${outputData.stderr}`);
|
|
125
|
+
})
|
|
126
|
+
]);
|
|
127
|
+
} else if (waitForExit) {
|
|
128
|
+
await proc.exited;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
process: proc,
|
|
133
|
+
stdout: outputData.stdout,
|
|
134
|
+
stderr: outputData.stderr,
|
|
135
|
+
exitCode: proc.exitCode ?? 0,
|
|
136
|
+
getStdout: () => outputData.stdout,
|
|
137
|
+
getStderr: () => outputData.stderr
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
runPackageScript: {
|
|
143
|
+
type: CapsulePropertyTypes.Function,
|
|
144
|
+
value: async function (this: any, options: any): Promise<any> {
|
|
145
|
+
const {
|
|
146
|
+
runtime = 'bun',
|
|
147
|
+
script,
|
|
148
|
+
args = [],
|
|
149
|
+
cwd,
|
|
150
|
+
env,
|
|
151
|
+
verbose = false
|
|
152
|
+
} = options;
|
|
153
|
+
|
|
154
|
+
const cmdArgs = [...args];
|
|
155
|
+
if (cmdArgs.length) {
|
|
156
|
+
cmdArgs.unshift('--')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const spawned = await this.spawnProcess({
|
|
160
|
+
cmd: [runtime, 'run', script, ...cmdArgs],
|
|
161
|
+
cwd,
|
|
162
|
+
waitForReady: false,
|
|
163
|
+
waitForExit: true,
|
|
164
|
+
env,
|
|
165
|
+
verbose
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
exitCode: spawned.exitCode,
|
|
170
|
+
stdout: spawned.stdout,
|
|
171
|
+
stderr: spawned.stderr
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
waitForFetch: {
|
|
177
|
+
type: CapsulePropertyTypes.Function,
|
|
178
|
+
value: async function (this: any, options: any): Promise<boolean | Response> {
|
|
179
|
+
const {
|
|
180
|
+
url,
|
|
181
|
+
method = 'GET',
|
|
182
|
+
headers,
|
|
183
|
+
body,
|
|
184
|
+
status,
|
|
185
|
+
retryDelayMs = 1000,
|
|
186
|
+
requestTimeoutMs = 2000,
|
|
187
|
+
timeoutMs = 30000,
|
|
188
|
+
verbose = false,
|
|
189
|
+
returnResponse = false
|
|
190
|
+
} = options;
|
|
191
|
+
|
|
192
|
+
const startTime = Date.now();
|
|
193
|
+
let attemptCount = 0;
|
|
194
|
+
|
|
195
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
196
|
+
attemptCount++;
|
|
197
|
+
const elapsed = Date.now() - startTime;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const response = await fetch(url, {
|
|
201
|
+
method,
|
|
202
|
+
headers,
|
|
203
|
+
body,
|
|
204
|
+
signal: AbortSignal.timeout(requestTimeoutMs)
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (status === true) {
|
|
208
|
+
if (verbose) {
|
|
209
|
+
console.log(`[waitForFetch] URL ${url} responded (status: ${response.status}) after ${attemptCount} attempts (${elapsed}ms)`);
|
|
210
|
+
}
|
|
211
|
+
return returnResponse ? response : true;
|
|
212
|
+
} else if (typeof status === 'number') {
|
|
213
|
+
if (response.status === status) {
|
|
214
|
+
if (verbose) {
|
|
215
|
+
console.log(`[waitForFetch] URL ${url} responded with status ${status} after ${attemptCount} attempts (${elapsed}ms)`);
|
|
216
|
+
}
|
|
217
|
+
return returnResponse ? response : true;
|
|
218
|
+
} else {
|
|
219
|
+
if (verbose) {
|
|
220
|
+
console.log(`[waitForFetch] Attempt ${attemptCount}: Got status ${response.status}, expected ${status} (${elapsed}ms)`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (status === false) {
|
|
226
|
+
if (verbose) {
|
|
227
|
+
console.log(`[waitForFetch] URL ${url} is not responding (as expected) after ${attemptCount} attempts (${elapsed}ms)`);
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
} else {
|
|
231
|
+
if (verbose) {
|
|
232
|
+
console.log(`[waitForFetch] Attempt ${attemptCount}: Request failed (${elapsed}ms)`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const remainingTime = timeoutMs - (Date.now() - startTime);
|
|
238
|
+
if (remainingTime > 0) {
|
|
239
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(retryDelayMs, remainingTime)));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (verbose) {
|
|
244
|
+
console.log(`[waitForFetch] Timeout reached after ${attemptCount} attempts (${Date.now() - startTime}ms)`);
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}, {
|
|
252
|
+
importMeta: import.meta,
|
|
253
|
+
importStack: makeImportStack(),
|
|
254
|
+
capsuleName: capsule['#']
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
capsule['#'] = '@stream44.studio/t44/caps/ProjectTestLib'
|
package/caps/RootKey.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
export async function capsule({
|
|
2
|
+
encapsulate,
|
|
3
|
+
CapsulePropertyTypes,
|
|
4
|
+
makeImportStack
|
|
5
|
+
}: {
|
|
6
|
+
encapsulate: any
|
|
7
|
+
CapsulePropertyTypes: any
|
|
8
|
+
makeImportStack: any
|
|
9
|
+
}) {
|
|
10
|
+
return encapsulate({
|
|
11
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
12
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
13
|
+
'#@stream44.studio/t44/structs/WorkspaceConfig': {
|
|
14
|
+
as: '$WorkspaceConfig'
|
|
15
|
+
},
|
|
16
|
+
'#@stream44.studio/t44/structs/RootKeyConfig': {
|
|
17
|
+
as: '$RootKeyConfig'
|
|
18
|
+
},
|
|
19
|
+
'#': {
|
|
20
|
+
Home: {
|
|
21
|
+
type: CapsulePropertyTypes.Mapping,
|
|
22
|
+
value: '@stream44.studio/t44/caps/Home'
|
|
23
|
+
},
|
|
24
|
+
WorkspacePrompt: {
|
|
25
|
+
type: CapsulePropertyTypes.Mapping,
|
|
26
|
+
value: '@stream44.studio/t44/caps/WorkspacePrompt'
|
|
27
|
+
},
|
|
28
|
+
ensureKey: {
|
|
29
|
+
type: CapsulePropertyTypes.Function,
|
|
30
|
+
value: async function (this: any): Promise<{ keyName: string; privateKeyPath: string; publicKey: string; keyFingerprint: string } | null> {
|
|
31
|
+
const { join } = await import('path')
|
|
32
|
+
const { stat: statFile } = await import('fs/promises')
|
|
33
|
+
const chalk = (await import('chalk')).default
|
|
34
|
+
const {
|
|
35
|
+
discoverEd25519Keys,
|
|
36
|
+
validateConfiguredKey,
|
|
37
|
+
ensurePassphrase,
|
|
38
|
+
ensureKeyInAgent,
|
|
39
|
+
computeFingerprint,
|
|
40
|
+
promptPassphrase,
|
|
41
|
+
generateEd25519Key
|
|
42
|
+
} = await import('../lib/key.js')
|
|
43
|
+
|
|
44
|
+
const workspaceConfig = await this.$WorkspaceConfig.config
|
|
45
|
+
const keyConfig = await this.$RootKeyConfig.config
|
|
46
|
+
const sshDir = await this.Home.sshDir
|
|
47
|
+
|
|
48
|
+
// --- Already configured: validate ---
|
|
49
|
+
if (keyConfig?.name && keyConfig?.privateKeyPath && keyConfig?.publicKey && keyConfig?.keyFingerprint) {
|
|
50
|
+
const valid = await validateConfiguredKey(keyConfig, 'Root Key')
|
|
51
|
+
if (!valid) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
keyName: keyConfig.name,
|
|
56
|
+
privateKeyPath: keyConfig.privateKeyPath,
|
|
57
|
+
publicKey: keyConfig.publicKey,
|
|
58
|
+
keyFingerprint: keyConfig.keyFingerprint
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- Not configured: discover or create ---
|
|
63
|
+
console.log(chalk.cyan(`\n🔑 Root Key Setup\n`))
|
|
64
|
+
console.log(chalk.gray(` Workspace: ${workspaceConfig?.name || 'unknown'}`))
|
|
65
|
+
console.log(chalk.gray(` Root: ${workspaceConfig?.rootDir || 'unknown'}`))
|
|
66
|
+
console.log(chalk.gray(''))
|
|
67
|
+
console.log(chalk.gray(' The root key is an Ed25519 SSH key used to identify this workspace.'))
|
|
68
|
+
console.log(chalk.gray(` You can select an existing key from ${sshDir} or create a new one.`))
|
|
69
|
+
console.log(chalk.gray(''))
|
|
70
|
+
|
|
71
|
+
// Discover existing Ed25519 keys in ~/.ssh
|
|
72
|
+
const existingKeys = await discoverEd25519Keys(sshDir)
|
|
73
|
+
|
|
74
|
+
// Build choices
|
|
75
|
+
const choices: Array<{ name: string; value: any }> = []
|
|
76
|
+
|
|
77
|
+
for (const key of existingKeys) {
|
|
78
|
+
choices.push({
|
|
79
|
+
name: `${key.name} ${chalk.gray(key.publicKey.substring(0, 60) + '...')}`,
|
|
80
|
+
value: { type: 'existing', ...key }
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
choices.push({
|
|
85
|
+
name: chalk.yellow('+ Create a new Ed25519 key'),
|
|
86
|
+
value: { type: 'create' }
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const selected = await this.WorkspacePrompt.select({
|
|
90
|
+
message: 'Select an Ed25519 key:',
|
|
91
|
+
choices,
|
|
92
|
+
defaultValue: { type: 'create' },
|
|
93
|
+
pageSize: 15
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
let keyName: string
|
|
97
|
+
let privateKeyPath: string
|
|
98
|
+
let publicKey: string
|
|
99
|
+
let keyFingerprint: string
|
|
100
|
+
|
|
101
|
+
if (selected.type === 'existing') {
|
|
102
|
+
keyName = selected.name
|
|
103
|
+
privateKeyPath = selected.privateKeyPath
|
|
104
|
+
publicKey = selected.publicKey
|
|
105
|
+
keyFingerprint = computeFingerprint(selected.privateKeyPath)
|
|
106
|
+
|
|
107
|
+
// Ensure selected existing key has a passphrase
|
|
108
|
+
const passphraseOk = await ensurePassphrase(privateKeyPath, keyName, 'Root key')
|
|
109
|
+
if (!passphraseOk) {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Add to ssh-agent
|
|
114
|
+
await ensureKeyInAgent(privateKeyPath, keyName, 'Root key')
|
|
115
|
+
} else {
|
|
116
|
+
// Prompt for key name
|
|
117
|
+
keyName = await this.WorkspacePrompt.input({
|
|
118
|
+
message: 'Enter a name for the new key:',
|
|
119
|
+
defaultValue: 'id_t44_ed25519',
|
|
120
|
+
validate: (input: string) => {
|
|
121
|
+
if (!input || input.trim().length === 0) {
|
|
122
|
+
return 'Key name cannot be empty'
|
|
123
|
+
}
|
|
124
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(input)) {
|
|
125
|
+
return 'Key name can only contain letters, numbers, underscores, dots, and hyphens'
|
|
126
|
+
}
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
privateKeyPath = join(sshDir, keyName)
|
|
132
|
+
|
|
133
|
+
// Check if file already exists
|
|
134
|
+
let exists = false
|
|
135
|
+
try {
|
|
136
|
+
await statFile(privateKeyPath)
|
|
137
|
+
exists = true
|
|
138
|
+
} catch { }
|
|
139
|
+
|
|
140
|
+
if (exists) {
|
|
141
|
+
console.log(chalk.red(`\n ✗ File already exists: ${privateKeyPath}`))
|
|
142
|
+
console.log(chalk.red(` Choose a different name or select the existing key.\n`))
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Prompt for passphrase
|
|
147
|
+
console.log(chalk.cyan(`\n Generating Ed25519 key '${keyName}'...`))
|
|
148
|
+
console.log(chalk.gray(` The key will be protected with a passphrase and added to the macOS Keychain.\n`))
|
|
149
|
+
|
|
150
|
+
const envPassphrase = process.env.T44_KEYS_PASSPHRASE
|
|
151
|
+
const passphrase = envPassphrase || await promptPassphrase()
|
|
152
|
+
if (!passphrase) {
|
|
153
|
+
console.log(chalk.red(`\n ✗ A passphrase is required for the root key.\n`))
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const result = await generateEd25519Key(privateKeyPath, passphrase, 't44-root-key')
|
|
158
|
+
if (!result) {
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
publicKey = result.publicKey
|
|
163
|
+
keyFingerprint = result.keyFingerprint
|
|
164
|
+
|
|
165
|
+
console.log(chalk.green(` ✓ Key generated:`))
|
|
166
|
+
console.log(chalk.green(` ${privateKeyPath}`))
|
|
167
|
+
console.log(chalk.green(` ${privateKeyPath}.pub\n`))
|
|
168
|
+
|
|
169
|
+
// Add the new key to the ssh-agent with Keychain storage
|
|
170
|
+
if (!envPassphrase) {
|
|
171
|
+
await ensureKeyInAgent(privateKeyPath, keyName, 'Root key')
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Store in config
|
|
176
|
+
await this.$RootKeyConfig.setConfigValue(['name'], keyName)
|
|
177
|
+
await this.$RootKeyConfig.setConfigValue(['privateKeyPath'], privateKeyPath)
|
|
178
|
+
await this.$RootKeyConfig.setConfigValue(['publicKey'], publicKey)
|
|
179
|
+
await this.$RootKeyConfig.setConfigValue(['keyFingerprint'], keyFingerprint)
|
|
180
|
+
|
|
181
|
+
console.log(chalk.green(` ✓ Root key configured: ${keyName}`))
|
|
182
|
+
console.log(chalk.green(` ${keyFingerprint}\n`))
|
|
183
|
+
|
|
184
|
+
return { keyName, privateKeyPath, publicKey, keyFingerprint }
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
getKeyPath: {
|
|
188
|
+
type: CapsulePropertyTypes.Function,
|
|
189
|
+
value: async function (this: any): Promise<string | null> {
|
|
190
|
+
const keyConfig = await this.$RootKeyConfig.config
|
|
191
|
+
|
|
192
|
+
if (!keyConfig?.privateKeyPath) {
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return keyConfig.privateKeyPath
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
getPublicKey: {
|
|
200
|
+
type: CapsulePropertyTypes.Function,
|
|
201
|
+
value: async function (this: any): Promise<string | null> {
|
|
202
|
+
const keyConfig = await this.$RootKeyConfig.config
|
|
203
|
+
|
|
204
|
+
if (!keyConfig?.publicKey) {
|
|
205
|
+
return null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return keyConfig.publicKey
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}, {
|
|
214
|
+
importMeta: import.meta,
|
|
215
|
+
importStack: makeImportStack(),
|
|
216
|
+
capsuleName: capsule['#'],
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
capsule['#'] = '@stream44.studio/t44/caps/RootKey'
|