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 +1 -1
- package/src/appollo/asc-launch.mjs +90 -0
- package/src/appollo/asc-run.mjs +8 -4
- package/src/appollo/index.mjs +2 -1
- package/src/checkouts.mjs +74 -0
- package/src/git.mjs +3 -3
- package/src/mac-tools.mjs +9 -1
- package/src/worktrees.mjs +1 -6
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/appollo/asc-run.mjs
CHANGED
|
@@ -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
|
-
|
|
27
|
+
let data = out
|
|
27
28
|
try {
|
|
28
|
-
|
|
29
|
+
data = out ? JSON.parse(out) : null
|
|
29
30
|
} catch {
|
|
30
|
-
|
|
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
|
})
|
package/src/appollo/index.mjs
CHANGED
|
@@ -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
|
-
|
|
69
|
-
|
|
70
|
-
const m = /github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)
|
|
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
|
|
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. */
|