@yagni-app/code-staging 1.1.4-staging.1412.1 → 1.1.4-staging.1414.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 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: {} };
@@ -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
- * - symlinks `dist/` (+ CHANGELOG/README/docs/examples) back to the real pi
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
- * - symlinks `dist/` (+ CHANGELOG/README/docs/examples) back to the real pi
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 symlinked only if it exists in the real package.
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
- symlinkSync(target, linkPath, type);
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
- linkInto(shadowDir, realPiDir, entry);
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.1412.1",
3
+ "version": "1.1.4-staging.1414.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": "4266630468e144c9ac86fc902b44a9de1b05409f"
61
+ "yagniSourceSha": "5d3561f59b32cd7bd24dc661dc2cf34bf4b5f40b"
62
62
  }