@yagni-app/code-staging 1.1.4-staging.1412.1 → 1.1.4-staging.1415.1
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/dist/cli.d.ts +8 -0
- package/dist/cli.js +16 -2
- package/dist/extension/diffStat.js +8 -1
- package/dist/piPackage.d.ts +16 -2
- package/dist/piPackage.js +61 -8
- package/package.json +2 -2
package/dist/cli.d.ts
CHANGED
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { type OutputFormat } from "./outputFormat.js";
|
|
16
16
|
import { promptKeepOrRemoveWorktree } from "./worktreeExitPrompt.js";
|
|
17
|
+
/**
|
|
18
|
+
* The stderr warning for a failed shadow-package build: fail-soft stands
|
|
19
|
+
* (never block a launch), but never silent — an un-rebranded session is
|
|
20
|
+
* exactly what the user is least able to self-diagnose (chrome reads "pi",
|
|
21
|
+
* the resume command says "pi --session …"). One helper, not one per catch
|
|
22
|
+
* site, so the message cannot drift between the two launch paths.
|
|
23
|
+
*/
|
|
24
|
+
export declare function shadowFailureWarning(err: unknown): string;
|
|
17
25
|
/**
|
|
18
26
|
* Seed `editorPaddingX` into the per-profile pi `settings.json` so the prompt
|
|
19
27
|
* input aligns with the chat/output area (`outputPad`) and the status bar.
|
package/dist/cli.js
CHANGED
|
@@ -46,6 +46,16 @@ import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir, resolveSes
|
|
|
46
46
|
import { credentialsFromProfile, getActiveProfileName, listProfiles, migrateLegacyCredentials, persistProfileTokenRotation, profilePath, readActiveProfile, useProfile, } from "./profiles.js";
|
|
47
47
|
// Present as "yagni" in process listings, not "node".
|
|
48
48
|
process.title = DISTRIBUTION.commandName;
|
|
49
|
+
/**
|
|
50
|
+
* The stderr warning for a failed shadow-package build: fail-soft stands
|
|
51
|
+
* (never block a launch), but never silent — an un-rebranded session is
|
|
52
|
+
* exactly what the user is least able to self-diagnose (chrome reads "pi",
|
|
53
|
+
* the resume command says "pi --session …"). One helper, not one per catch
|
|
54
|
+
* site, so the message cannot drift between the two launch paths.
|
|
55
|
+
*/
|
|
56
|
+
export function shadowFailureWarning(err) {
|
|
57
|
+
return `Could not prepare the YAGNI Code package shadow; this session will show "pi" branding: ${err instanceof Error ? err.message : String(err)}\n`;
|
|
58
|
+
}
|
|
49
59
|
/** The one-time Claude-compat trust question, asked on the launch TTY. */
|
|
50
60
|
async function confirmOnTty(question) {
|
|
51
61
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -230,9 +240,11 @@ async function runDefault(passthroughArgs) {
|
|
|
230
240
|
realPiDir: resolvePiPackageDir(),
|
|
231
241
|
shadowDir: piPackageDir(),
|
|
232
242
|
name: PI_CONFIG_NAME,
|
|
243
|
+
onNote: (m) => process.stderr.write(`[yagni] ${m}\n`),
|
|
233
244
|
});
|
|
234
245
|
}
|
|
235
|
-
catch {
|
|
246
|
+
catch (err) {
|
|
247
|
+
process.stderr.write(shadowFailureWarning(err));
|
|
236
248
|
shadowPiDir = undefined;
|
|
237
249
|
}
|
|
238
250
|
// Zero-config Claude Code compat: wire this repo's (and the user's)
|
|
@@ -436,9 +448,11 @@ async function runWorktreeLaunch(passthroughArgs, worktreeName, loadSessionWorkt
|
|
|
436
448
|
realPiDir: resolvePiPackageDir(),
|
|
437
449
|
shadowDir: piPackageDir(),
|
|
438
450
|
name: PI_CONFIG_NAME,
|
|
451
|
+
onNote: (m) => process.stderr.write(`[yagni] ${m}\n`),
|
|
439
452
|
});
|
|
440
453
|
}
|
|
441
|
-
catch {
|
|
454
|
+
catch (err) {
|
|
455
|
+
process.stderr.write(shadowFailureWarning(err));
|
|
442
456
|
shadowPiDir = undefined;
|
|
443
457
|
}
|
|
444
458
|
let compat = { argv: [], env: {} };
|
|
@@ -141,14 +141,21 @@ export function formatDiffStat(diff) {
|
|
|
141
141
|
export function createDiffStatCache(workDir, run = runGit) {
|
|
142
142
|
const isRepo = detectGitRepo(run, workDir);
|
|
143
143
|
let cached = null;
|
|
144
|
+
// Distinct from `cached === null`: null is a legitimate, cacheable answer
|
|
145
|
+
// (no resolvable default branch, unrelated histories, a git failure). Keying
|
|
146
|
+
// freshness off the VALUE made every render re-run readDiffStat — 3-7 `git`
|
|
147
|
+
// spawns per keystroke, which is ~5ms each on macOS but 30-80ms on Windows,
|
|
148
|
+
// where it dominates typing latency. Key off whether we have fetched instead.
|
|
149
|
+
let fetched = false;
|
|
144
150
|
let fetchedAt = 0;
|
|
145
151
|
return {
|
|
146
152
|
get() {
|
|
147
153
|
if (!isRepo)
|
|
148
154
|
return null;
|
|
149
155
|
const now = Date.now();
|
|
150
|
-
if (
|
|
156
|
+
if (!fetched || now - fetchedAt >= STATUS_TTL_MS) {
|
|
151
157
|
cached = readDiffStat(run, workDir);
|
|
158
|
+
fetched = true;
|
|
152
159
|
fetchedAt = now;
|
|
153
160
|
}
|
|
154
161
|
return cached;
|
package/dist/piPackage.d.ts
CHANGED
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
* reinstall) or copying pi wholesale, we generate a tiny shadow dir that:
|
|
9
9
|
*
|
|
10
10
|
* - owns its own `package.json` (pi's, with `piConfig.name` injected), and
|
|
11
|
-
* -
|
|
12
|
-
* package, so asset/theme resolution still finds the real files.
|
|
11
|
+
* - links `dist/` (+ CHANGELOG/README/docs/examples) back to the real pi
|
|
12
|
+
* package, so asset/theme resolution still finds the real files. The link
|
|
13
|
+
* is a symlink where privileges allow; on stock Windows
|
|
14
|
+
* (`CreateSymbolicLinkW` needs `SeCreateSymbolicLinkPrivilege`) dirs fall
|
|
15
|
+
* back to junctions (privilege-free) and any entry that still can't be
|
|
16
|
+
* linked is copied.
|
|
13
17
|
*
|
|
14
18
|
* The launcher points `PI_PACKAGE_DIR` at this dir. Regenerated every launch so
|
|
15
19
|
* a pi upgrade or store-path move can never leave a stale package.json or a
|
|
@@ -22,6 +26,16 @@ export interface ShadowPiPackageOptions {
|
|
|
22
26
|
shadowDir: string;
|
|
23
27
|
/** The `piConfig.name` to inject — what pi shows as its app name. */
|
|
24
28
|
name: string;
|
|
29
|
+
/**
|
|
30
|
+
* Diagnostic sink for a copy fallback (no link form worked, so the shadow
|
|
31
|
+
* materialized as real copies — slower to rebuild each launch, and able to
|
|
32
|
+
* serve stale content between refreshes). Called ONCE per build, naming
|
|
33
|
+
* the first copied DIR entry (file copies are silent everywhere — on
|
|
34
|
+
* stock Windows they are the normal case). Omit to stay silent; this
|
|
35
|
+
* module never touches stderr itself, so embedded consumers (the desktop
|
|
36
|
+
* driver) can't be surprised by output they can't suppress.
|
|
37
|
+
*/
|
|
38
|
+
onNote?: (message: string) => void;
|
|
25
39
|
}
|
|
26
40
|
/**
|
|
27
41
|
* Materialize (idempotently) the shadow pi package and return its path.
|
package/dist/piPackage.js
CHANGED
|
@@ -8,20 +8,24 @@
|
|
|
8
8
|
* reinstall) or copying pi wholesale, we generate a tiny shadow dir that:
|
|
9
9
|
*
|
|
10
10
|
* - owns its own `package.json` (pi's, with `piConfig.name` injected), and
|
|
11
|
-
* -
|
|
12
|
-
* package, so asset/theme resolution still finds the real files.
|
|
11
|
+
* - links `dist/` (+ CHANGELOG/README/docs/examples) back to the real pi
|
|
12
|
+
* package, so asset/theme resolution still finds the real files. The link
|
|
13
|
+
* is a symlink where privileges allow; on stock Windows
|
|
14
|
+
* (`CreateSymbolicLinkW` needs `SeCreateSymbolicLinkPrivilege`) dirs fall
|
|
15
|
+
* back to junctions (privilege-free) and any entry that still can't be
|
|
16
|
+
* linked is copied.
|
|
13
17
|
*
|
|
14
18
|
* The launcher points `PI_PACKAGE_DIR` at this dir. Regenerated every launch so
|
|
15
19
|
* a pi upgrade or store-path move can never leave a stale package.json or a
|
|
16
20
|
* dangling symlink behind.
|
|
17
21
|
*/
|
|
18
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
22
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
19
23
|
import { join } from "node:path";
|
|
20
24
|
// Entries pi resolves relative to its package dir. `dist` carries theme + TUI
|
|
21
25
|
// assets + export templates (the load-bearing ones); the rest back /changelog,
|
|
22
|
-
// /about, etc. Each is
|
|
26
|
+
// /about, etc. Each is linked only if it exists in the real package.
|
|
23
27
|
const LINKED_ENTRIES = ["dist", "CHANGELOG.md", "README.md", "docs", "examples"];
|
|
24
|
-
function linkInto(shadowDir, realPiDir, entry) {
|
|
28
|
+
function linkInto(shadowDir, realPiDir, entry, onNote) {
|
|
25
29
|
const target = join(realPiDir, entry);
|
|
26
30
|
if (!existsSync(target))
|
|
27
31
|
return;
|
|
@@ -30,6 +34,9 @@ function linkInto(shadowDir, realPiDir, entry) {
|
|
|
30
34
|
// unlinked directly: rmSync stats THROUGH it, so on a dangling link (target
|
|
31
35
|
// checkout deleted) it sees ENOENT, force-swallows it, and leaves the link
|
|
32
36
|
// in place - then symlinkSync throws EEXIST and the rebrand silently drops.
|
|
37
|
+
// Junctions are lstat-visible as symlinks on POSIX and win32 alike (the
|
|
38
|
+
// Windows CI lane proved this), so both fall in here and are unlinked
|
|
39
|
+
// directly — never rmSync'd through into the real pi dist.
|
|
33
40
|
if (isSymlink(linkPath)) {
|
|
34
41
|
unlinkSync(linkPath);
|
|
35
42
|
}
|
|
@@ -37,7 +44,39 @@ function linkInto(shadowDir, realPiDir, entry) {
|
|
|
37
44
|
rmSync(linkPath, { recursive: true, force: true });
|
|
38
45
|
}
|
|
39
46
|
const type = statSync(target).isDirectory() ? "dir" : "file";
|
|
40
|
-
|
|
47
|
+
try {
|
|
48
|
+
symlinkSync(target, linkPath, type);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
// Stock Windows denies CreateSymbolicLinkW (EPERM) without
|
|
52
|
+
// SeCreateSymbolicLinkPrivilege. Directory junctions need no privilege,
|
|
53
|
+
// so retry dirs as junctions (the type is ignored on POSIX); anything
|
|
54
|
+
// else (files) or a still-denied dir falls back to a copy.
|
|
55
|
+
if (type === "dir") {
|
|
56
|
+
try {
|
|
57
|
+
symlinkSync(target, linkPath, "junction");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// fall through to the copy
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// The last resort: a real copy — unguarded cpSync is the throw's own
|
|
65
|
+
// delivery: a failure here propagates out of ensureShadowPiPackage, so a
|
|
66
|
+
// half-built shadow (rebranded package.json over a missing dist) never
|
|
67
|
+
// ships — the launcher's fail-soft catch gives pi the working un-rebranded
|
|
68
|
+
// package instead of a broken session.
|
|
69
|
+
cpSync(target, linkPath, { recursive: true, dereference: true, force: true });
|
|
70
|
+
// Noted for DIR entries only. On stock Windows files can never link (no
|
|
71
|
+
// privilege-free file equivalent of a junction exists), so CHANGELOG/
|
|
72
|
+
// README copy on EVERY launch there — noting them would fire the note on
|
|
73
|
+
// every stock-Windows launch, not just on a degraded build. A dir
|
|
74
|
+
// falling all the way to a copy is the real signal (the expensive,
|
|
75
|
+
// load-bearing degradation), so that is what earns the note.
|
|
76
|
+
if (type === "dir") {
|
|
77
|
+
onNote(`could not link '${entry}' into the package shadow (${err instanceof Error ? err.message : String(err)}); copied instead`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
41
80
|
}
|
|
42
81
|
function isSymlink(p) {
|
|
43
82
|
try {
|
|
@@ -54,7 +93,7 @@ function isSymlink(p) {
|
|
|
54
93
|
* derive pi's version/name, and a silent fallback would re-leak "pi".
|
|
55
94
|
*/
|
|
56
95
|
export function ensureShadowPiPackage(opts) {
|
|
57
|
-
const { realPiDir, shadowDir, name } = opts;
|
|
96
|
+
const { realPiDir, shadowDir, name, onNote } = opts;
|
|
58
97
|
const realPkgRaw = readFileSync(join(realPiDir, "package.json"), "utf8");
|
|
59
98
|
const realPkg = JSON.parse(realPkgRaw);
|
|
60
99
|
mkdirSync(shadowDir, { recursive: true });
|
|
@@ -63,8 +102,22 @@ export function ensureShadowPiPackage(opts) {
|
|
|
63
102
|
piConfig: { ...(realPkg.piConfig ?? {}), name },
|
|
64
103
|
};
|
|
65
104
|
writeFileSync(join(shadowDir, "package.json"), `${JSON.stringify(shadowPkg, null, 2)}\n`);
|
|
105
|
+
// One note per BUILD, not per entry: whichever entry FIRST degrades to a
|
|
106
|
+
// copy gets named (in practice `dist` — first in LINKED_ENTRIES, and the
|
|
107
|
+
// load-bearing one), and one line saying the shadow degraded to copies is
|
|
108
|
+
// all the diagnosis needs — near-identical lines for every later entry
|
|
109
|
+
// would just be noise.
|
|
110
|
+
let noted = false;
|
|
66
111
|
for (const entry of LINKED_ENTRIES) {
|
|
67
|
-
|
|
112
|
+
if (noted) {
|
|
113
|
+
linkInto(shadowDir, realPiDir, entry, () => { });
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
linkInto(shadowDir, realPiDir, entry, (message) => {
|
|
117
|
+
onNote?.(message);
|
|
118
|
+
noted = true;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
68
121
|
}
|
|
69
122
|
return shadowDir;
|
|
70
123
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.1.4-staging.
|
|
3
|
+
"version": "1.1.4-staging.1415.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"turndown": "^7.2.4",
|
|
59
59
|
"typebox": "^1.3.15"
|
|
60
60
|
},
|
|
61
|
-
"yagniSourceSha": "
|
|
61
|
+
"yagniSourceSha": "f4fe956b10a09436d8429ae3f6b82cfa73a8d880"
|
|
62
62
|
}
|