pi-landstrip 0.11.8 → 0.11.9
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/README.md +2 -1
- package/index.ts +135 -100
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,10 +34,11 @@ Use Pi settings to toggle sandboxing:
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Project Pi settings override global Pi settings.
|
|
37
|
+
The `/sandbox` UI updates the project setting when present, otherwise the global setting.
|
|
37
38
|
|
|
38
39
|
## Usage
|
|
39
40
|
|
|
40
|
-
Use `/sandbox` inside Pi to show the active config.
|
|
41
|
+
Use `/sandbox` inside Pi to show the active config and toggle the Pi setting.
|
|
41
42
|
|
|
42
43
|
## License
|
|
43
44
|
|
package/index.ts
CHANGED
|
@@ -63,11 +63,14 @@ interface SandboxConfig {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
interface PiLandstripSettings {
|
|
66
|
+
[key: string]: unknown;
|
|
66
67
|
landstrip?: {
|
|
67
68
|
enabled?: boolean;
|
|
68
69
|
};
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
type PiSettingsScope = 'global' | 'project';
|
|
73
|
+
|
|
71
74
|
interface LandstripPolicy {
|
|
72
75
|
network: {
|
|
73
76
|
allowNetwork: boolean;
|
|
@@ -190,14 +193,53 @@ function loadConfig(cwd: string): SandboxConfig {
|
|
|
190
193
|
return deepMerge(deepMerge(DEFAULT_CONFIG, globalConfig), projectConfig);
|
|
191
194
|
}
|
|
192
195
|
|
|
193
|
-
function
|
|
196
|
+
function getPiLandstripSettings(cwd: string): {
|
|
197
|
+
globalSettings: PiLandstripSettings;
|
|
198
|
+
projectSettings: PiLandstripSettings;
|
|
199
|
+
} {
|
|
194
200
|
const settings = SettingsManager.create(cwd);
|
|
195
|
-
|
|
196
|
-
|
|
201
|
+
return {
|
|
202
|
+
globalSettings: settings.getGlobalSettings() as PiLandstripSettings,
|
|
203
|
+
projectSettings: settings.getProjectSettings() as PiLandstripSettings,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
197
206
|
|
|
207
|
+
function isSandboxEnabledInPiSettings(cwd: string): boolean {
|
|
208
|
+
const { globalSettings, projectSettings } = getPiLandstripSettings(cwd);
|
|
198
209
|
return projectSettings.landstrip?.enabled ?? globalSettings.landstrip?.enabled ?? true;
|
|
199
210
|
}
|
|
200
211
|
|
|
212
|
+
function getPiSettingsWriteScope(cwd: string): PiSettingsScope {
|
|
213
|
+
const { projectSettings } = getPiLandstripSettings(cwd);
|
|
214
|
+
return projectSettings.landstrip?.enabled === undefined ? 'global' : 'project';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function getPiSettingsPath(cwd: string, scope: PiSettingsScope): string {
|
|
218
|
+
if (scope === 'project') return join(cwd, '.pi', 'settings.json');
|
|
219
|
+
return join(getAgentDir(), 'settings.json');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function readPiSettingsFile(settingsPath: string): PiLandstripSettings {
|
|
223
|
+
if (!existsSync(settingsPath)) return {};
|
|
224
|
+
return JSON.parse(readFileSync(settingsPath, 'utf-8')) as PiLandstripSettings;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function setPiSettingsSandboxEnabled(
|
|
228
|
+
cwd: string,
|
|
229
|
+
enabled: boolean,
|
|
230
|
+
): Promise<PiSettingsScope> {
|
|
231
|
+
const scope = getPiSettingsWriteScope(cwd);
|
|
232
|
+
const settingsPath = getPiSettingsPath(cwd, scope);
|
|
233
|
+
await withFileMutationQueue(settingsPath, async () => {
|
|
234
|
+
const settings = readPiSettingsFile(settingsPath);
|
|
235
|
+
settings.landstrip = { ...settings.landstrip, enabled };
|
|
236
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
237
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return scope;
|
|
241
|
+
}
|
|
242
|
+
|
|
201
243
|
function mergeArray(base: string[], override?: string[]): string[] {
|
|
202
244
|
if (!override) return base;
|
|
203
245
|
return [...new Set([...base, ...override])];
|
|
@@ -1610,10 +1652,7 @@ export function createLandstripIntegration(
|
|
|
1610
1652
|
maybePi.registerCommand?.('sandbox', {
|
|
1611
1653
|
description: 'Show sandbox configuration',
|
|
1612
1654
|
handler: async (_args, ctx) => {
|
|
1613
|
-
|
|
1614
|
-
notify(ctx, 'Sandbox is disabled', 'info');
|
|
1615
|
-
return;
|
|
1616
|
-
}
|
|
1655
|
+
let piSettingsEnabled = isSandboxEnabledInPiSettings(ctx.cwd);
|
|
1617
1656
|
|
|
1618
1657
|
const config = loadConfig(ctx.cwd);
|
|
1619
1658
|
const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
|
|
@@ -1627,6 +1666,14 @@ export function createLandstripIntegration(
|
|
|
1627
1666
|
const text = (s: string) => theme.fg('text', s);
|
|
1628
1667
|
const borderFg = (s: string) => theme.fg('border', s);
|
|
1629
1668
|
|
|
1669
|
+
function sandboxStatus(): { color: 'success' | 'warning'; label: string } {
|
|
1670
|
+
if (noSandboxFlag) return { color: 'warning', label: 'Disabled (--no-sandbox)' };
|
|
1671
|
+
if (!piSettingsEnabled) return { color: 'warning', label: 'Disabled (Pi setting)' };
|
|
1672
|
+
if (!config.enabled) return { color: 'warning', label: 'Disabled (config)' };
|
|
1673
|
+
if (!sandboxEnabled || !sandboxReady) return { color: 'warning', label: 'Inactive' };
|
|
1674
|
+
return { color: 'success', label: 'Active' };
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1630
1677
|
function boolVal(v: boolean): string {
|
|
1631
1678
|
return v ? theme.fg('warning', 'yes') : theme.fg('success', 'no');
|
|
1632
1679
|
}
|
|
@@ -1639,120 +1686,108 @@ export function createLandstripIntegration(
|
|
|
1639
1686
|
|
|
1640
1687
|
return {
|
|
1641
1688
|
render(width: number): string[] {
|
|
1642
|
-
const innerW = width - 4;
|
|
1689
|
+
const innerW = Math.max(1, width - 4);
|
|
1643
1690
|
const border = borderFg('│');
|
|
1644
|
-
const row = (
|
|
1691
|
+
const row = (content: string) => makeRow(content, innerW, border);
|
|
1645
1692
|
const lines: string[] = [];
|
|
1693
|
+
const status = sandboxStatus();
|
|
1694
|
+
const toggleValue = piSettingsEnabled
|
|
1695
|
+
? theme.fg('success', 'enabled')
|
|
1696
|
+
: theme.fg('warning', 'disabled');
|
|
1697
|
+
|
|
1698
|
+
function topBorder(titleText: string): string {
|
|
1699
|
+
const title = accent(` ${titleText} `);
|
|
1700
|
+
const fill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(title))));
|
|
1701
|
+
return `${borderFg('╭─')}${title}${fill}${borderFg('─╮')}`;
|
|
1702
|
+
}
|
|
1646
1703
|
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1704
|
+
function section(titleText: string, detail?: string): void {
|
|
1705
|
+
lines.push(row(''));
|
|
1706
|
+
lines.push(row(`${accent(titleText)}${detail ? dim(` · ${detail}`) : ''}`));
|
|
1707
|
+
}
|
|
1651
1708
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
lines.push(
|
|
1656
|
-
row(
|
|
1657
|
-
` ${statusDot} ${text('Active')} ${dim('·')} ${muted('landstrip:')} ${pathSnippet}`,
|
|
1658
|
-
),
|
|
1659
|
-
);
|
|
1709
|
+
function item(label: string, value: string): void {
|
|
1710
|
+
lines.push(row(` ${dim('•')} ${muted(label.padEnd(13))} ${value}`));
|
|
1711
|
+
}
|
|
1660
1712
|
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1713
|
+
function listValue(values: string[], maxWidth: number): string {
|
|
1714
|
+
const value = values.join(', ') || 'none';
|
|
1715
|
+
return text(truncateToWidth(value, Math.max(10, maxWidth)));
|
|
1716
|
+
}
|
|
1665
1717
|
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
const
|
|
1670
|
-
lines.push(row(` ${accent('Network')}${dim(netMode)}`));
|
|
1671
|
-
lines.push(
|
|
1672
|
-
row(
|
|
1673
|
-
` ${dim('•')} ${muted('Allow network:')} ${boolVal(config.network.allowNetwork)}`,
|
|
1674
|
-
),
|
|
1675
|
-
);
|
|
1676
|
-
const domainsStr = config.network.allowedDomains.join(', ') || '(none)';
|
|
1677
|
-
lines.push(
|
|
1678
|
-
row(
|
|
1679
|
-
` ${dim('•')} ${muted('Allowed:')} ${text(truncateToWidth(domainsStr, Math.max(10, innerW - 15)))}`,
|
|
1680
|
-
),
|
|
1681
|
-
);
|
|
1682
|
-
const denyStr = config.network.deniedDomains.join(', ') || '(none)';
|
|
1718
|
+
lines.push(topBorder('Sandbox'));
|
|
1719
|
+
|
|
1720
|
+
const statusDot = theme.fg(status.color, '●');
|
|
1721
|
+
const pathSnippet = text(truncateToWidth(binaryPath(), Math.max(20, innerW - 28)));
|
|
1683
1722
|
lines.push(
|
|
1684
1723
|
row(
|
|
1685
|
-
|
|
1724
|
+
`${statusDot} ${text(status.label)} ${dim('·')} Pi setting ${toggleValue} ${dim('·')} ${muted('landstrip')} ${pathSnippet}`,
|
|
1686
1725
|
),
|
|
1687
1726
|
);
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1727
|
+
|
|
1728
|
+
section('Config');
|
|
1729
|
+
item('project', text(projectPath));
|
|
1730
|
+
item('global', text(globalPath));
|
|
1731
|
+
|
|
1732
|
+
const netMode = config.network.allowNetwork ? 'unrestricted' : 'proxied';
|
|
1733
|
+
section('Network', netMode);
|
|
1734
|
+
item('allow network', boolVal(config.network.allowNetwork));
|
|
1735
|
+
item('allowed', listValue(config.network.allowedDomains, innerW - 17));
|
|
1736
|
+
item('denied', listValue(config.network.deniedDomains, innerW - 17));
|
|
1737
|
+
if (sessionAllowedDomains.length > 0)
|
|
1738
|
+
item('session', theme.fg('accent', sessionAllowedDomains.join(', ')));
|
|
1739
|
+
|
|
1740
|
+
section('Filesystem');
|
|
1741
|
+
item('deny read', listValue(config.filesystem.denyRead, innerW - 17));
|
|
1742
|
+
item('allow read', listValue(config.filesystem.allowRead, innerW - 17));
|
|
1743
|
+
item('allow write', listValue(config.filesystem.allowWrite, innerW - 17));
|
|
1744
|
+
item('deny write', listValue(config.filesystem.denyWrite, innerW - 17));
|
|
1745
|
+
|
|
1746
|
+
if (sessionAllowedReadPaths.length > 0 || sessionAllowedWritePaths.length > 0) {
|
|
1747
|
+
section('Session grants');
|
|
1748
|
+
if (sessionAllowedReadPaths.length > 0)
|
|
1749
|
+
item('read', theme.fg('accent', sessionAllowedReadPaths.join(', ')));
|
|
1750
|
+
if (sessionAllowedWritePaths.length > 0)
|
|
1751
|
+
item('write', theme.fg('accent', sessionAllowedWritePaths.join(', ')));
|
|
1694
1752
|
}
|
|
1695
1753
|
|
|
1696
|
-
// Filesystem section
|
|
1697
1754
|
lines.push(row(''));
|
|
1698
|
-
lines.push(row(`${'─'.repeat(innerW)}`));
|
|
1699
|
-
lines.push(row(` ${accent('Filesystem')}`));
|
|
1700
|
-
const denyReadStr = config.filesystem.denyRead.join(', ') || '(none)';
|
|
1701
|
-
lines.push(
|
|
1702
|
-
row(
|
|
1703
|
-
` ${dim('•')} ${muted('Deny read:')} ${text(truncateToWidth(denyReadStr, Math.max(10, innerW - 16)))}`,
|
|
1704
|
-
),
|
|
1705
|
-
);
|
|
1706
|
-
const allowReadStr = config.filesystem.allowRead.join(', ') || '(none)';
|
|
1707
|
-
lines.push(
|
|
1708
|
-
row(
|
|
1709
|
-
` ${dim('•')} ${muted('Allow read:')} ${text(truncateToWidth(allowReadStr, Math.max(10, innerW - 17)))}`,
|
|
1710
|
-
),
|
|
1711
|
-
);
|
|
1712
|
-
const allowWriteStr = config.filesystem.allowWrite.join(', ') || '(none)';
|
|
1713
1755
|
lines.push(
|
|
1714
|
-
row(
|
|
1715
|
-
` ${dim('•')} ${muted('Allow write:')} ${text(truncateToWidth(allowWriteStr, Math.max(10, innerW - 18)))}`,
|
|
1716
|
-
),
|
|
1756
|
+
row(`${dim('t')} ${muted('toggle Pi setting')} ${dim('esc')} ${muted('close')}`),
|
|
1717
1757
|
);
|
|
1718
|
-
const denyWriteStr = config.filesystem.denyWrite.join(', ') || '(none)';
|
|
1719
1758
|
lines.push(
|
|
1720
|
-
|
|
1721
|
-
` ${dim('•')} ${muted('Deny write:')} ${text(truncateToWidth(denyWriteStr, Math.max(10, innerW - 17)))}`,
|
|
1722
|
-
),
|
|
1759
|
+
`${borderFg('╰')}${borderFg('─'.repeat(Math.max(0, width - 2)))}${borderFg('╯')}`,
|
|
1723
1760
|
);
|
|
1724
1761
|
|
|
1725
|
-
// Session allowances
|
|
1726
|
-
if (sessionAllowedReadPaths.length > 0 || sessionAllowedWritePaths.length > 0) {
|
|
1727
|
-
lines.push(row(''));
|
|
1728
|
-
if (sessionAllowedReadPaths.length > 0) {
|
|
1729
|
-
lines.push(
|
|
1730
|
-
row(
|
|
1731
|
-
` ${dim('•')} ${muted('Session read:')} ${theme.fg('accent', sessionAllowedReadPaths.join(', '))}`,
|
|
1732
|
-
),
|
|
1733
|
-
);
|
|
1734
|
-
}
|
|
1735
|
-
if (sessionAllowedWritePaths.length > 0) {
|
|
1736
|
-
lines.push(
|
|
1737
|
-
row(
|
|
1738
|
-
` ${dim('•')} ${muted('Session write:')} ${theme.fg('accent', sessionAllowedWritePaths.join(', '))}`,
|
|
1739
|
-
),
|
|
1740
|
-
);
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
// Footer
|
|
1745
|
-
lines.push(row(''));
|
|
1746
|
-
lines.push(row(` ${dim('esc')} ${muted('or any key to close')}`));
|
|
1747
|
-
|
|
1748
|
-
// Bottom border
|
|
1749
|
-
lines.push(`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`);
|
|
1750
|
-
|
|
1751
1762
|
return lines;
|
|
1752
1763
|
},
|
|
1753
1764
|
|
|
1754
|
-
handleInput(): void {
|
|
1755
|
-
|
|
1765
|
+
handleInput(data: string): void {
|
|
1766
|
+
if (data !== 't' && data !== 'T') {
|
|
1767
|
+
done(undefined);
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
void (async () => {
|
|
1772
|
+
const enabled = !piSettingsEnabled;
|
|
1773
|
+
const scope = await setPiSettingsSandboxEnabled(ctx.cwd, enabled);
|
|
1774
|
+
piSettingsEnabled = isSandboxEnabledInPiSettings(ctx.cwd);
|
|
1775
|
+
|
|
1776
|
+
if (!enabled) {
|
|
1777
|
+
disableSandbox(ctx);
|
|
1778
|
+
notify(ctx, `Sandbox disabled via ${scope} Pi settings`, 'info');
|
|
1779
|
+
} else if (noSandboxFlag) {
|
|
1780
|
+
notify(ctx, 'Sandbox remains disabled via --no-sandbox', 'warning');
|
|
1781
|
+
} else if (!config.enabled) {
|
|
1782
|
+
notify(ctx, 'Sandbox remains disabled via config', 'info');
|
|
1783
|
+
} else if (enableSandbox(ctx)) {
|
|
1784
|
+
notify(ctx, `Sandbox enabled via ${scope} Pi settings`, 'info');
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
tui.requestRender();
|
|
1788
|
+
})().catch((error: unknown) => {
|
|
1789
|
+
notify(ctx, `Could not update Pi settings: ${error}`, 'error');
|
|
1790
|
+
});
|
|
1756
1791
|
},
|
|
1757
1792
|
|
|
1758
1793
|
invalidate(): void {},
|