t44 0.4.0-rc.13 → 0.4.0-rc.14

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.
@@ -0,0 +1,26 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [ "main" ]
6
+ pull_request:
7
+ branches: [ "main" ]
8
+
9
+ jobs:
10
+ test:
11
+ name: Run Tests
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - name: Checkout code
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Setup Bun
18
+ uses: oven-sh/setup-bun@v2
19
+ with:
20
+ bun-version: latest
21
+
22
+ - name: Install dependencies
23
+ run: bun install
24
+
25
+ - name: Run tests
26
+ run: bun test
@@ -71,7 +71,7 @@ export async function capsule({
71
71
  type: CapsulePropertyTypes.Function,
72
72
  value: async function (this: any, { args }: any): Promise<void> {
73
73
 
74
- const { projectSelector, rc, release, bump, publish, dangerouslyResetMain, yesSignoff } = args
74
+ const { projectSelector, rc, release, bump, publish, dangerouslyResetMain, dangerouslyResetGordianOpenIntegrity, yesSignoff } = args
75
75
 
76
76
  // Determine if this is a dry-run (default) or actual publish
77
77
  const isDryRun = !rc && !release && !bump && !publish
@@ -149,7 +149,8 @@ export async function capsule({
149
149
  await this.ProjectRepository.sync({
150
150
  rootDir: repoSourceDir,
151
151
  sourceDir: projectSourceDir,
152
- gitignorePath
152
+ gitignorePath,
153
+ excludePatterns: repositoriesConfig.alwaysIgnore || []
153
154
  })
154
155
 
155
156
  stageSourceDirs.set(repoName, repoSourceDir)
@@ -433,8 +434,9 @@ export async function capsule({
433
434
  })
434
435
  } else if (capsuleName === 't44/caps/providers/git-scm.com/ProjectPublishing' && !isDryRun) {
435
436
  await this.GitRepository.push({
436
- config: { ...repoConfig, provider: providerConfig, sourceDir: repoSourceDir },
437
+ config: { ...repoConfig, provider: providerConfig, sourceDir: repoSourceDir, alwaysIgnore: repositoriesConfig.alwaysIgnore },
437
438
  dangerouslyResetMain,
439
+ dangerouslyResetGordianOpenIntegrity,
438
440
  yesSignoff,
439
441
  metadata: gitMetadata.get(repoName),
440
442
  projectSourceDir: (repoConfig as any).sourceDir
@@ -62,10 +62,11 @@ export async function capsule({
62
62
  },
63
63
  sync: {
64
64
  type: CapsulePropertyTypes.Function,
65
- value: async function (this: any, { rootDir, sourceDir, gitignorePath }: {
65
+ value: async function (this: any, { rootDir, sourceDir, gitignorePath, excludePatterns }: {
66
66
  rootDir: string
67
67
  sourceDir: string
68
68
  gitignorePath?: string
69
+ excludePatterns?: string[]
69
70
  }): Promise<void> {
70
71
  let gitignoreExists = false
71
72
  if (gitignorePath) {
@@ -79,6 +80,12 @@ export async function capsule({
79
80
  if (gitignoreExists && gitignorePath) {
80
81
  rsyncArgs.push('--exclude-from=' + gitignorePath)
81
82
  }
83
+ // Add additional exclude patterns from alwaysIgnore config
84
+ if (excludePatterns && excludePatterns.length > 0) {
85
+ for (const pattern of excludePatterns) {
86
+ rsyncArgs.push('--exclude', pattern)
87
+ }
88
+ }
82
89
  rsyncArgs.push(sourceDir + '/', rootDir + '/')
83
90
  await $`${rsyncArgs}`
84
91
  }
@@ -55,6 +55,8 @@ extends:
55
55
  value: optional
56
56
  dangerously-reset-main:
57
57
  description: Reset the git repository and force push to remote.
58
+ dangerously-reset-gordian-open-integrity:
59
+ description: Reset the Gordian Open Integrity trust root.
58
60
  yes-signoff:
59
61
  description: Automatically agree to DCO sign-off without prompting.
60
62
  deploy:
@@ -10,6 +10,9 @@ import { join } from 'path'
10
10
  const shownConnectionTitles = new Set<string>()
11
11
  const shownDescriptions = new Set<string>()
12
12
 
13
+ // Cache for in-flight getStoredConfig promises to prevent parallel decryption race conditions
14
+ const storedConfigCache = new Map<string, Promise<Record<string, any> | null>>()
15
+
13
16
  export async function capsule({
14
17
  encapsulate,
15
18
  CapsulePropertyTypes,
@@ -77,9 +80,29 @@ export async function capsule({
77
80
  getStoredConfig: {
78
81
  type: CapsulePropertyTypes.Function,
79
82
  value: async function (this: any): Promise<Record<string, any> | null> {
80
- const { readFile } = await import('fs/promises')
81
83
  const filepath = await this.getFilepath()
82
84
 
85
+ // Use cached promise if already in-flight to prevent parallel decryption race conditions
86
+ if (storedConfigCache.has(filepath)) {
87
+ return storedConfigCache.get(filepath)!
88
+ }
89
+
90
+ const promise = this._getStoredConfigImpl(filepath)
91
+ storedConfigCache.set(filepath, promise)
92
+
93
+ try {
94
+ return await promise
95
+ } finally {
96
+ // Clear cache after completion so next call gets fresh data
97
+ storedConfigCache.delete(filepath)
98
+ }
99
+ }
100
+ },
101
+ _getStoredConfigImpl: {
102
+ type: CapsulePropertyTypes.Function,
103
+ value: async function (this: any, filepath: string): Promise<Record<string, any> | null> {
104
+ const { readFile } = await import('fs/promises')
105
+
83
106
  try {
84
107
  const content = await readFile(filepath, 'utf-8')
85
108
  const parsed = JSON.parse(content)
@@ -1,7 +1,7 @@
1
-
2
1
  import type * as BunTest from 'bun:test'
3
2
  import { config as loadDotenv } from 'dotenv'
4
- import { join } from 'path'
3
+ import { join, dirname, basename } from 'path'
4
+ import { mkdir } from 'fs/promises'
5
5
 
6
6
  // Global cache for loaded env files (this is fine as a cache)
7
7
  const loadedEnvFiles = new Set<string>()
@@ -67,6 +67,35 @@ export async function capsule({
67
67
  return process.env[envVarName]
68
68
  }
69
69
  },
70
+ workbenchDir: {
71
+ type: CapsulePropertyTypes.GetterFunction,
72
+ value: function (this: any): string {
73
+
74
+ const moduleFilepath = this['#@stream44.studio/encapsulate/structs/Capsule'].rootCapsule.moduleFilepath
75
+ const dir = join(dirname(moduleFilepath), '.~o/workspace.foundation/workbenches', basename(moduleFilepath).replace(/\.[^\.]+$/, ''))
76
+
77
+ return dir
78
+ }
79
+ },
80
+ emptyWorkbenchDir: {
81
+ type: CapsulePropertyTypes.Function,
82
+ value: async function (this: any): Promise<void> {
83
+ const dir = this.workbenchDir
84
+
85
+ // Ensure the directory exists first
86
+ await mkdir(dir, { recursive: true })
87
+
88
+ // Remove directory contents (not the directory itself) including dotfiles
89
+ // Use shell with proper globbing to handle both regular files and dotfiles
90
+ await Bun.$`sh -c 'rm -rf ${dir}/* ${dir}/.[!.]* ${dir}/..?* 2>/dev/null || true'`.quiet()
91
+ }
92
+ },
93
+ EnsureEmptyWorkbenchDir: {
94
+ type: CapsulePropertyTypes.StructInit,
95
+ value: async function (this: any) {
96
+ await this.emptyWorkbenchDir()
97
+ }
98
+ },
70
99
  describe: {
71
100
  type: CapsulePropertyTypes.GetterFunction,
72
101
  value: function (this: any) {
@@ -161,7 +190,7 @@ export async function capsule({
161
190
  }, {
162
191
  importMeta: import.meta,
163
192
  importStack: makeImportStack(),
164
- capsuleName: capsule['#'],
193
+ capsuleName: capsule['#']
165
194
  })
166
195
  }
167
196
  capsule['#'] = 't44/caps/WorkspaceTest'
@@ -90,7 +90,8 @@ export async function capsule({
90
90
  await this.ProjectRepository.sync({
91
91
  rootDir: stageDir,
92
92
  sourceDir: projectSourceDir,
93
- gitignorePath
93
+ gitignorePath,
94
+ excludePatterns: config.alwaysIgnore || []
94
95
  })
95
96
 
96
97
  // Generate files from config properties starting with '/'
@@ -195,7 +196,7 @@ export async function capsule({
195
196
  },
196
197
  push: {
197
198
  type: CapsulePropertyTypes.Function,
198
- value: async function (this: any, { config, dangerouslyResetMain, yesSignoff, metadata, projectSourceDir }: { config: any, dangerouslyResetMain?: boolean, yesSignoff?: boolean, metadata: any, projectSourceDir?: string }) {
199
+ value: async function (this: any, { config, dangerouslyResetMain, dangerouslyResetGordianOpenIntegrity, yesSignoff, metadata, projectSourceDir }: { config: any, dangerouslyResetMain?: boolean, dangerouslyResetGordianOpenIntegrity?: boolean, yesSignoff?: boolean, metadata: any, projectSourceDir?: string }) {
199
200
 
200
201
  const {
201
202
  originUri,
@@ -327,25 +328,15 @@ export async function capsule({
327
328
  await rm(join(dir!, '.dco-signatures'), { force: true })
328
329
  }
329
330
 
330
- // Create OI identity using the workspace signing key
331
- console.log(`Creating GordianOpenIntegrity identity ...`)
332
- console.log(chalk.gray(` Author: ${authorName} <${authorEmail}>`))
333
- const author = await this.GordianOpenIntegrity.createIdentity({
334
- key: {
335
- privateKeyPath: signingKeyPath,
336
- publicKeyPath: `${signingKeyPath}.pub`,
337
- publicKey: signingPublicKey,
338
- fingerprint: signingFingerprint,
339
- },
340
- authorName,
341
- authorEmail,
342
- })
343
-
344
- // Create OI inception repo at stageDir
331
+ // Create OI inception repo at stageDir using the workspace signing key
345
332
  console.log(`Creating GordianOpenIntegrity inception repository ...`)
333
+ console.log(chalk.gray(` Author: ${authorName} <${authorEmail}>`))
346
334
  const repoResult = await this.GordianOpenIntegrity.createRepository({
347
335
  repoDir: stageDir,
348
- author,
336
+ authorName,
337
+ authorEmail,
338
+ firstTrustKeyPath: signingKeyPath,
339
+ provenanceKeyPath: signingKeyPath,
349
340
  })
350
341
  console.log(chalk.green(` ✓ Inception commit: ${repoResult.commitHash.slice(0, 8)}`))
351
342
  console.log(chalk.green(` ✓ DID: ${repoResult.did}`))
@@ -434,7 +425,8 @@ export async function capsule({
434
425
  await this.ProjectRepository.sync({
435
426
  rootDir: stageDir,
436
427
  sourceDir: projectSourcePath,
437
- gitignorePath
428
+ gitignorePath,
429
+ excludePatterns: config.alwaysIgnore || []
438
430
  })
439
431
 
440
432
  // Generate files from config properties starting with '/'
@@ -454,7 +446,7 @@ export async function capsule({
454
446
  const hasDco = await this.Dco.hasDco({ repoDir: stageDir })
455
447
  if (hasDco) {
456
448
  console.log(chalk.cyan(`DCO.md detected — running DCO signing process ...`))
457
- await this.Dco.sign({ repoDir: stageDir, autoAgree: yesSignoff, signingKeyPath: author.sshKey.privateKeyPath })
449
+ await this.Dco.sign({ repoDir: stageDir, autoAgree: yesSignoff, signingKeyPath })
458
450
  }
459
451
 
460
452
  // Stage all files and commit as a signed commit
@@ -462,7 +454,9 @@ export async function capsule({
462
454
  await $`git add -A`.cwd(stageDir).quiet()
463
455
  await this.GordianOpenIntegrity.commitToRepository({
464
456
  repoDir: stageDir,
465
- author,
457
+ authorName,
458
+ authorEmail,
459
+ signingKeyPath,
466
460
  message: 'Published using @Stream44 Studio',
467
461
  })
468
462
  console.log(chalk.green(` ✓ Source content committed`))
@@ -516,7 +510,113 @@ export async function capsule({
516
510
  })
517
511
  console.log(`Repository reset to initial commit`)
518
512
  }
519
- } else if (hasNewChanges) {
513
+ }
514
+
515
+ // Handle Gordian Open Integrity trust root reset (without deleting git history)
516
+ if (dangerouslyResetGordianOpenIntegrity && oiEnabled && !dangerouslyResetMain) {
517
+ console.log(chalk.cyan(`\nResetting Gordian Open Integrity trust root ...`))
518
+
519
+ // Get author info from workspace.yaml config
520
+ const authorConfig = config.provider?.config?.RepositorySettings?.author
521
+ if (!authorConfig?.name || !authorConfig?.email) {
522
+ throw new Error('GordianOpenIntegrity requires author.name and author.email in RepositorySettings config')
523
+ }
524
+ const authorName = authorConfig.name
525
+ const authorEmail = authorConfig.email
526
+
527
+ // Resolve the workspace signing key
528
+ const signingKeyPath = await this.SigningKey.getKeyPath()
529
+ const signingPublicKey = await this.SigningKey.getPublicKey()
530
+ const signingFingerprint = await this.SigningKey.getFingerprint()
531
+ const signingKeyName = await this.SigningKey.getKeyName()
532
+ if (!signingKeyPath || !signingPublicKey || !signingFingerprint) {
533
+ throw new Error('Signing key not configured. Run SigningKey.ensureKey() first.')
534
+ }
535
+ console.log(chalk.gray(` Signing key: ${signingKeyName} (${signingKeyPath})`))
536
+ console.log(chalk.gray(` Author: ${authorName} <${authorEmail}>`))
537
+
538
+ // Check if .repo-identifier exists to decide which method to call
539
+ const repoIdentifierPath = join(stageDir, '.repo-identifier')
540
+ let repoIdentifierExists = false
541
+ try {
542
+ await access(repoIdentifierPath, constants.F_OK)
543
+ repoIdentifierExists = true
544
+ } catch {
545
+ // File doesn't exist
546
+ }
547
+
548
+ let repoResult: any
549
+ if (repoIdentifierExists) {
550
+ // .repo-identifier exists — reset trust root only
551
+ console.log(chalk.gray(` Found existing .repo-identifier — resetting trust root only`))
552
+ repoResult = await this.GordianOpenIntegrity.createTrustRoot({
553
+ repoDir: stageDir,
554
+ authorName,
555
+ authorEmail,
556
+ firstTrustKeyPath: signingKeyPath,
557
+ provenanceKeyPath: signingKeyPath,
558
+ })
559
+ } else {
560
+ // No .repo-identifier — create full repository (identifier + trust root)
561
+ console.log(chalk.gray(` No .repo-identifier found — creating repository identifier and trust root`))
562
+ repoResult = await this.GordianOpenIntegrity.createRepository({
563
+ repoDir: stageDir,
564
+ authorName,
565
+ authorEmail,
566
+ firstTrustKeyPath: signingKeyPath,
567
+ provenanceKeyPath: signingKeyPath,
568
+ })
569
+ }
570
+ console.log(chalk.green(` ✓ New trust root created`))
571
+ console.log(chalk.green(` ✓ DID: ${repoResult.did}`))
572
+
573
+ // Copy .o/GordianOpenIntegrity.yaml and lifehash images to source directories
574
+ const stageODir = join(stageDir, '.o')
575
+ const projectSourcePath = join(config.sourceDir)
576
+
577
+ const prStageInceptionDir = join(projectSourcePath, '.o')
578
+ await mkdir(prStageInceptionDir, { recursive: true })
579
+ await copyFile(join(stageODir, 'GordianOpenIntegrity.yaml'), join(prStageInceptionDir, 'GordianOpenIntegrity.yaml'))
580
+ for (const lifehashFile of ['GordianOpenIntegrity-InceptionLifehash.svg', 'GordianOpenIntegrity-CurrentLifehash.svg']) {
581
+ await copyFile(join(stageODir, lifehashFile), join(prStageInceptionDir, lifehashFile))
582
+ }
583
+
584
+ if (projectSourceDir) {
585
+ const sourceInceptionDir = join(projectSourceDir, '.o')
586
+ await mkdir(sourceInceptionDir, { recursive: true })
587
+ await copyFile(join(stageODir, 'GordianOpenIntegrity.yaml'), join(sourceInceptionDir, 'GordianOpenIntegrity.yaml'))
588
+ for (const lifehashFile of ['GordianOpenIntegrity-InceptionLifehash.svg', 'GordianOpenIntegrity-CurrentLifehash.svg']) {
589
+ await copyFile(join(stageODir, lifehashFile), join(sourceInceptionDir, lifehashFile))
590
+ }
591
+ }
592
+ console.log(chalk.green(` ✓ Copied .o/GordianOpenIntegrity.yaml and lifehash images to source directories`))
593
+
594
+ // Update Repository DID in README.md files if present
595
+ const DID_PATTERN = /^(Repository DID: `)([^`]*)(`)$/m
596
+ for (const dir of [stageDir, projectSourcePath, projectSourceDir].filter(Boolean)) {
597
+ const readmePath = join(dir!, 'README.md')
598
+ try {
599
+ const readmeContent = await readFile(readmePath, 'utf-8')
600
+ if (DID_PATTERN.test(readmeContent)) {
601
+ const updated = readmeContent.replace(DID_PATTERN, `$1${repoResult.did}$3`)
602
+ await writeFile(readmePath, updated, 'utf-8')
603
+ console.log(chalk.green(` ✓ Updated Repository DID in ${readmePath}`))
604
+ }
605
+ } catch { }
606
+ }
607
+
608
+ // Store generator in the registry
609
+ const registryRootDir = await this.HomeRegistry.rootDir
610
+ const oiRegistryDir = join(registryRootDir, OI_REGISTRY_CAPSULE, repoResult.did)
611
+ await mkdir(oiRegistryDir, { recursive: true })
612
+
613
+ const repoGeneratorPath = join(stageDir, GENERATOR_FILE)
614
+ const registryGeneratorPath = join(oiRegistryDir, 'GordianOpenIntegrity-generator.yaml')
615
+ await cp(repoGeneratorPath, registryGeneratorPath)
616
+ console.log(chalk.green(` ✓ Generator stored at: ${registryGeneratorPath}`))
617
+ }
618
+
619
+ if (!dangerouslyResetMain && hasNewChanges) {
520
620
  // Check if DCO.md exists in the stage dir
521
621
  const hasDco = await this.Dco.hasDco({ repoDir: stageDir })
522
622
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "t44",
3
- "version": "0.4.0-rc.13",
3
+ "version": "0.4.0-rc.14",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,6 +10,9 @@
10
10
  "bin": {
11
11
  "t44": "./bin/t44"
12
12
  },
13
+ "scripts": {
14
+ "test": "bun test"
15
+ },
13
16
  "exports": {
14
17
  "./workspace-rt": "./workspace-rt.ts",
15
18
  "./standalone-rt": "./standalone-rt.ts",
@@ -85,9 +88,9 @@
85
88
  "@ucanto/principal": "^9.0.3",
86
89
  "@ucanto/server": "^11.0.3",
87
90
  "json-schema-ref-resolver": "^3.0.0",
88
- "@stream44.studio/encapsulate": "^0.4.0-rc.11",
89
- "@stream44.studio/dco": "^0.3.0-rc.11",
90
- "@stream44.studio/t44-blockchaincommons.com": "^0.1.0-rc.12"
91
+ "@stream44.studio/encapsulate": "^0.4.0-rc.12",
92
+ "@stream44.studio/dco": "^0.3.0-rc.12",
93
+ "@stream44.studio/t44-blockchaincommons.com": "^0.1.0-rc.13"
91
94
  },
92
95
  "devDependencies": {
93
96
  "@types/bun": "^1.3.4",
package/standalone-rt.ts CHANGED
@@ -38,7 +38,7 @@ export async function run(encapsulateHandler: any, runHandler: any, options?: {
38
38
 
39
39
  const { encapsulate, freeze, CapsulePropertyTypes, makeImportStack, hoistSnapshot } = await CapsuleSpineFactory({
40
40
  spineFilesystemRoot,
41
- capsuleModuleProjectionRoot: (import.meta as any).dir,
41
+ capsuleModuleProjectionRoot: options?.importMeta?.dir!,
42
42
  enableCallerStackInference: true,
43
43
  spineContracts: {
44
44
  ['#' + CapsuleSpineContract['#']]: CapsuleSpineContract