kira-glasses-bridge 2.28.0 → 2.29.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kira-glasses-bridge",
3
- "version": "2.28.0",
3
+ "version": "2.29.0",
4
4
  "description": "Connects your Mac to Kira, the assistant for Even G2 glasses: Claude Code sessions, files, folders and apps, by voice.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,90 @@
1
+ // Appollo (POC): getting an app in App Store Connect to Apple's review. Reads asc's release
2
+ // dashboard and its submission readiness report; the only writes are three fixed fixes the person
3
+ // starts from the page (price Free, every country, the reviewer's sign-in). Also finds the app's
4
+ // code on this Mac by its bundle id.
5
+ import { execFile } from 'node:child_process'
6
+ import { homedir } from 'node:os'
7
+ import { join } from 'node:path'
8
+ import { run } from './asc-run.mjs'
9
+
10
+ const APP_ID = /^\d{6,12}$/
11
+ const UUID = /^[0-9a-f-]{36}$/i
12
+ const BUNDLE_ID = /^[A-Za-z0-9.-]{3,155}$/
13
+ const FIND_MS = 20_000
14
+ const FIND_ROOT = join(homedir(), 'Developer')
15
+
16
+ /** The project files that name an app's bundle id: Xcode, XcodeGen, Expo. */
17
+ const PROJECT_FILES = ['project.pbxproj', 'project.yml', 'app.json', 'app.config.ts', 'app.config.js']
18
+ const SKIP_DIRS = ['node_modules', 'Pods', 'DerivedData', '.git', 'build', 'dist', '.expo']
19
+
20
+ function appId(args) {
21
+ const id = String(args.app_id ?? '')
22
+ if (!APP_ID.test(id)) throw new Error('not an App Store Connect app id')
23
+ return id
24
+ }
25
+
26
+ /** The project folder a project file belongs to (the folder holding the .xcodeproj, or app.json's). */
27
+ function projectFolder(file) {
28
+ const parts = file.split('/')
29
+ const xcodeproj = parts.findIndex(p => p.endsWith('.xcodeproj'))
30
+ return (xcodeproj > 0 ? parts.slice(0, xcodeproj) : parts.slice(0, -1)).join('/')
31
+ }
32
+
33
+ export const ascLaunchTools = {
34
+ /** asc's release dashboard and readiness report for one app, as they come. */
35
+ async asc_launch_readiness(args) {
36
+ const id = appId(args)
37
+ const [status, report] = await Promise.all([
38
+ run(['status', '--app', id, '--output', 'json']),
39
+ run(['validate', '--app', id, '--deep', '--output', 'json'], { timeout: 120_000 }),
40
+ ])
41
+ return { status: status.data ?? null, report: report.data ?? null, error: status.error ?? report.error ?? null }
42
+ },
43
+
44
+ /** One of the fixes the person starts: price Free, every country, or the reviewer's sign-in. */
45
+ async asc_launch_fix(args) {
46
+ const id = appId(args)
47
+ const write = argv => run([...argv, '--output', 'json'], { readOnly: false })
48
+ if (args.action === 'free')
49
+ return write(['pricing', 'schedule', 'create', '--app', id, '--free', '--base-territory', 'US'])
50
+ if (args.action === 'everywhere')
51
+ return write([
52
+ 'pricing', 'availability', 'create', '--app', id, '--all-territories', '--available', 'true',
53
+ '--available-in-new-territories', 'true', '--if-exists', 'update',
54
+ ])
55
+ if (args.action === 'reviewer') {
56
+ const detail = String(args.detail_id ?? '')
57
+ const name = String(args.name ?? '').trim()
58
+ const password = String(args.password ?? '')
59
+ if (!UUID.test(detail) || !name || !password || password.length > 100)
60
+ return { error: 'the reviewer needs a username and a password (100 characters or fewer)' }
61
+ // The password goes to asc through its environment (@env:), never on a command line.
62
+ return run(
63
+ [
64
+ 'review', 'details-update', '--id', detail, '--demo-account-required', 'true',
65
+ '--demo-account-name', name, '--demo-account-password', '@env:APPOLLO_REVIEWER_PASSWORD', '--output', 'json',
66
+ ],
67
+ { readOnly: false, extraEnv: { APPOLLO_REVIEWER_PASSWORD: password } },
68
+ )
69
+ }
70
+ return { error: 'not a fix Appollo knows' }
71
+ },
72
+
73
+ /** Folders under ~/Developer whose project names this bundle id. */
74
+ appollo_find_project(args) {
75
+ const bundleId = String(args.bundle_id ?? '')
76
+ if (!BUNDLE_ID.test(bundleId)) return Promise.resolve({ error: 'not a bundle id' })
77
+ const grep = [
78
+ '-rlF', ...PROJECT_FILES.map(f => `--include=${f}`), ...SKIP_DIRS.map(d => `--exclude-dir=${d}`),
79
+ '-e', `"${bundleId}"`, '-e', `= ${bundleId};`, '-e', `: ${bundleId}`, FIND_ROOT,
80
+ ]
81
+ return new Promise(resolve => {
82
+ execFile('grep', grep, { timeout: FIND_MS, maxBuffer: 1024 * 1024 }, (_err, stdout) => {
83
+ const all = [...new Set(String(stdout ?? '').split('\n').filter(Boolean).map(projectFolder))]
84
+ // An Expo app's ios/ folder names the bundle id too: keep only the outermost project.
85
+ const folders = all.filter(f => !all.some(o => o !== f && f.startsWith(`${o}/`)))
86
+ resolve({ folders: folders.slice(0, 5).map(f => f.replace(homedir(), '~')) })
87
+ })
88
+ })
89
+ },
90
+ }
@@ -1,5 +1,6 @@
1
1
  // Running the asc CLI on this Mac (Appollo's way to App Store Connect): reads with ASC_READ_ONLY=1
2
- // unless told otherwise, answered as JSON.
2
+ // unless told otherwise, answered as JSON. A report that fails on purpose (asc validate exits 1 when
3
+ // something blocks submission) still answers with its JSON.
3
4
  import { execFile } from 'node:child_process'
4
5
  import { existsSync } from 'node:fs'
5
6
 
@@ -23,12 +24,15 @@ export function run(args, { readOnly = true, extraEnv = {}, timeout = READ_TIMEO
23
24
  const out = String(stdout ?? '').trim()
24
25
  if (err && err.code === 'ENOENT')
25
26
  return resolve({ error: 'asc is not installed on this Mac (brew install asc)', missing: true })
26
- if (err) return resolve({ error: (String(stderr ?? '').trim() || err.message).slice(0, 2000) })
27
+ let data = out
27
28
  try {
28
- resolve({ data: out ? JSON.parse(out) : null })
29
+ data = out ? JSON.parse(out) : null
29
30
  } catch {
30
- resolve({ data: out })
31
+ // Not JSON: asc's text.
31
32
  }
33
+ if (err && !(data && typeof data === 'object'))
34
+ return resolve({ error: (String(stderr ?? '').trim() || err.message).slice(0, 2000) })
35
+ resolve({ data })
32
36
  },
33
37
  )
34
38
  })
@@ -1,7 +1,8 @@
1
1
  // Appollo (POC) on this Mac: App Store Connect through asc, each app's workspace in ~/Appollo, and
2
2
  // the Doctor's checks. One export, so the bridge registers Appollo in one line (and drops it in one).
3
3
  import { ascTools } from './asc.mjs'
4
+ import { ascLaunchTools } from './asc-launch.mjs'
4
5
  import { doctorTools } from './doctor.mjs'
5
6
  import { appolloTools } from './workspace.mjs'
6
7
 
7
- export const appolloBridgeTools = { ...ascTools, ...appolloTools, ...doctorTools }
8
+ export const appolloBridgeTools = { ...ascTools, ...ascLaunchTools, ...appolloTools, ...doctorTools }
@@ -0,0 +1,74 @@
1
+ // Where a GitHub repo is already checked out on this Mac, so Kira links that folder instead of guessing a
2
+ // path or cloning another copy: the folders Claude Code has worked in (and the checkouts they sit in),
3
+ // then a shallow look through the usual places people keep code. Reads .git/config, runs nothing.
4
+ import { readdir, readFile, stat } from 'node:fs/promises'
5
+ import { homedir } from 'node:os'
6
+ import { dirname, join } from 'node:path'
7
+ import { listProjects } from './claude-sessions.mjs'
8
+ import { display, remoteToRepo } from './git.mjs'
9
+
10
+ const ROOTS = ['Developer', 'code', 'src', 'Projects', 'dev', 'repos', 'work']
11
+ const DEPTH = 3
12
+ const BUDGET_MS = 4000
13
+ const SKIP = new Set(['node_modules', 'Library', 'Applications', 'Pictures', 'Music', 'Movies'])
14
+
15
+ /** The origin's URL in a .git/config's text, or null. */
16
+ export function originOf(config) {
17
+ let inOrigin = false
18
+ for (const raw of String(config ?? '').split('\n')) {
19
+ const line = raw.trim()
20
+ if (line.startsWith('[')) inOrigin = /^\[remote\s+"origin"\]$/.test(line)
21
+ else if (inOrigin) {
22
+ const m = /^url\s*=\s*(.+)$/.exec(line)
23
+ if (m) return m[1].trim()
24
+ }
25
+ }
26
+ return null
27
+ }
28
+
29
+ const sameRepo = (a, b) => !!a && !!b && a.toLowerCase() === b.toLowerCase()
30
+ const isDir = p => stat(p).then(s => s.isDirectory()).catch(() => false)
31
+
32
+ /** The repo a checkout's origin points at (a main checkout: .git is a folder, not a worktree's file). */
33
+ async function repoAt(dir) {
34
+ if (!(await isDir(join(dir, '.git')))) return null
35
+ return remoteToRepo(originOf(await readFile(join(dir, '.git', 'config'), 'utf8').catch(() => '')))
36
+ }
37
+
38
+ /** The checkout a folder sits in (itself or a parent), or null. */
39
+ async function checkoutAround(dir) {
40
+ for (let d = dir; d && d !== dirname(d); d = dirname(d)) {
41
+ if (await isDir(join(d, '.git'))) return d
42
+ }
43
+ return null
44
+ }
45
+
46
+ /** Checkouts under a folder, a few levels down; a checkout isn't looked inside. */
47
+ async function scan(dir, depth, until, found) {
48
+ if (depth < 0 || Date.now() > until) return
49
+ if (await isDir(join(dir, '.git'))) return found.add(dir)
50
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => [])
51
+ for (const e of entries) {
52
+ if (!e.isDirectory() || e.name.startsWith('.') || SKIP.has(e.name)) continue
53
+ await scan(join(dir, e.name), depth - 1, until, found)
54
+ }
55
+ }
56
+
57
+ /** Every checkout of `repo` ("owner/name") on this Mac, as ~ paths; Kira's own worktrees aren't. */
58
+ export async function findCheckouts(repo, { home = homedir(), known = null } = {}) {
59
+ const candidates = new Set()
60
+ const worked = known ?? ((await listProjects(50).catch(() => ({ projects: [] }))).projects ?? []).map(p => p.cwd)
61
+ for (const cwd of worked) {
62
+ const top = await checkoutAround(cwd)
63
+ if (top) candidates.add(top)
64
+ }
65
+ const until = Date.now() + BUDGET_MS
66
+ for (const root of ROOTS) await scan(join(home, root), DEPTH, until, candidates)
67
+ const kira = join(home, '.kira')
68
+ const matches = []
69
+ for (const dir of candidates) {
70
+ if (dir.startsWith(kira)) continue
71
+ if (sameRepo(await repoAt(dir), repo)) matches.push(dir)
72
+ }
73
+ return { repo, checkouts: [...new Set(matches)].sort().map(display) }
74
+ }
package/src/git.mjs CHANGED
@@ -65,9 +65,9 @@ export async function snapshot(dir) {
65
65
  }
66
66
  }
67
67
 
68
- function remoteToRepo(url) {
69
- if (!url) return null
70
- const m = /github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?$/.exec(url)
68
+ /** "owner/name" from a GitHub remote URL (ssh or https), or null. */
69
+ export function remoteToRepo(url) {
70
+ const m = /github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/.exec(String(url ?? '').trim())
71
71
  return m ? m[1] : null
72
72
  }
73
73
 
package/src/mac-tools.mjs CHANGED
@@ -1,9 +1,10 @@
1
- // The bridge's tools for this Mac itself: its apps, a terminal, a new project's folder, and
1
+ // The bridge's tools for this Mac itself: its apps, a terminal, a project's folder (found or new), and
2
2
  // (inside the Mac app only) its shells for other devices and its browser dock.
3
3
  import { execFile } from 'node:child_process'
4
4
  import { openApp, installedApps } from './apps.mjs'
5
5
  import { resolveUserPath } from './files.mjs'
6
6
  import { display, setupProjectFolder } from './git.mjs'
7
+ import { findCheckouts } from './checkouts.mjs'
7
8
  import { terminalCall } from './terminals.mjs'
8
9
  import { safeCommand, terminalScript } from './terminal-command.mjs'
9
10
  import { host } from './host.mjs'
@@ -28,6 +29,13 @@ export const macTools = {
28
29
  })
29
30
  },
30
31
 
32
+ /** Where a repo ("owner/name") is already checked out here, so a project links it instead of cloning. */
33
+ async find_checkouts(args) {
34
+ const repo = String(args.repo ?? '').trim()
35
+ if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) return { error: 'repo must be "owner/name"' }
36
+ return findCheckouts(repo)
37
+ },
38
+
31
39
  /** This Mac's shells for a browser elsewhere (only inside the Mac app, only when switched on there). */
32
40
  terminal: args => terminalCall(args),
33
41
 
package/src/worktrees.mjs CHANGED
@@ -9,7 +9,7 @@ import { mkdir, stat, readFile, writeFile, appendFile, rm } from 'node:fs/promis
9
9
  import { pipeline } from 'node:stream/promises'
10
10
  import { createWriteStream } from 'node:fs'
11
11
  import { Readable } from 'node:stream'
12
- import { display } from './git.mjs'
12
+ import { display, remoteToRepo } from './git.mjs'
13
13
  import { resolveUserPath } from './files.mjs'
14
14
 
15
15
  const ROOT = join(homedir(), '.kira', 'worktrees')
@@ -24,11 +24,6 @@ function git(cwd, args, { timeout = 60_000 } = {}) {
24
24
  })
25
25
  }
26
26
 
27
- function remoteToRepo(url) {
28
- const m = /github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?$/.exec(url ?? '')
29
- return m ? m[1] : null
30
- }
31
-
32
27
  const sameRepo = (a, b) => !!a && !!b && a.toLowerCase() === b.toLowerCase()
33
28
 
34
29
  /** The GitHub repo a folder's origin points at, or null. */