@cmetech/otto 1.0.7 → 1.0.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/dist/cli-args.d.ts +2 -0
- package/dist/cli-args.js +6 -0
- package/dist/cli.js +27 -0
- package/dist/help-text.js +15 -0
- package/dist/onboarding.d.ts +19 -0
- package/dist/onboarding.js +133 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/otto/commands/release-notes/_data.js +155 -0
- package/dist/resources/extensions/otto/commands/release-notes/command.js +114 -0
- package/dist/resources/extensions/otto/extension-manifest.json +1 -1
- package/dist/resources/extensions/otto/index.js +3 -0
- package/dist/seed-defaults.d.ts +50 -0
- package/dist/seed-defaults.js +226 -0
- package/package.json +8 -7
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +3 -3
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +3 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +17 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +89 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +105 -67
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.js +20 -2
- package/packages/pi-coding-agent/dist/core/resolve-config-value.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +25 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +41 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +2 -2
- package/packages/pi-coding-agent/src/core/agent-session.ts +3 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +90 -1
- package/packages/pi-coding-agent/src/core/package-manager.ts +131 -64
- package/packages/pi-coding-agent/src/core/resolve-config-value.ts +20 -2
- package/packages/pi-coding-agent/src/core/settings-manager.ts +56 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/scripts/install.js +5 -2
- package/src/resources/extensions/otto/commands/release-notes/_data.ts +171 -0
- package/src/resources/extensions/otto/commands/release-notes/command.ts +141 -0
- package/src/resources/extensions/otto/extension-manifest.json +1 -1
- package/src/resources/extensions/otto/index.ts +4 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Extension runner - executes extensions and manages their lifecycle.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
6
|
import type { AgentMessage } from "@otto/pi-agent-core";
|
|
6
7
|
import type { ImageContent, Model } from "@otto/pi-ai";
|
|
7
8
|
import type { KeyId } from "@otto/pi-tui";
|
|
@@ -222,6 +223,49 @@ const noOpUIContext: ExtensionUIContext = {
|
|
|
222
223
|
setToolsExpanded: () => {},
|
|
223
224
|
};
|
|
224
225
|
|
|
226
|
+
// Async context flag: true when the current async stack originates inside a
|
|
227
|
+
// quiet extension's handler invocation. Read by the patched console methods to
|
|
228
|
+
// decide whether to suppress output. Empty store = normal logging behaviour.
|
|
229
|
+
const quietExtensionContext = new AsyncLocalStorage<boolean>();
|
|
230
|
+
|
|
231
|
+
let quietConsolePatchInstalled = false;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Patch `console.log/warn/error/info` to no-op when the current async context
|
|
235
|
+
* is flagged as "quiet". Installed once on first `setQuietExtensions(non-empty)`
|
|
236
|
+
* call so users who never configure quietExtensions pay zero overhead.
|
|
237
|
+
*
|
|
238
|
+
* The check is `quietExtensionContext.getStore() === true` — for any code path
|
|
239
|
+
* that wasn't dispatched inside `quietExtensionContext.run(true, …)` the store
|
|
240
|
+
* is undefined and console behaves exactly as before. This isolates the
|
|
241
|
+
* suppression to the quiet handler's own async work without affecting
|
|
242
|
+
* concurrent non-quiet handlers.
|
|
243
|
+
*/
|
|
244
|
+
function installQuietConsolePatch(): void {
|
|
245
|
+
if (quietConsolePatchInstalled) return;
|
|
246
|
+
quietConsolePatchInstalled = true;
|
|
247
|
+
const realLog = console.log.bind(console);
|
|
248
|
+
const realWarn = console.warn.bind(console);
|
|
249
|
+
const realError = console.error.bind(console);
|
|
250
|
+
const realInfo = console.info.bind(console);
|
|
251
|
+
console.log = (...args: unknown[]): void => {
|
|
252
|
+
if (quietExtensionContext.getStore() === true) return;
|
|
253
|
+
realLog(...args);
|
|
254
|
+
};
|
|
255
|
+
console.warn = (...args: unknown[]): void => {
|
|
256
|
+
if (quietExtensionContext.getStore() === true) return;
|
|
257
|
+
realWarn(...args);
|
|
258
|
+
};
|
|
259
|
+
console.error = (...args: unknown[]): void => {
|
|
260
|
+
if (quietExtensionContext.getStore() === true) return;
|
|
261
|
+
realError(...args);
|
|
262
|
+
};
|
|
263
|
+
console.info = (...args: unknown[]): void => {
|
|
264
|
+
if (quietExtensionContext.getStore() === true) return;
|
|
265
|
+
realInfo(...args);
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
225
269
|
export class ExtensionRunner {
|
|
226
270
|
private extensions: Extension[];
|
|
227
271
|
private runtime: ExtensionRuntime;
|
|
@@ -257,6 +301,8 @@ export class ExtensionRunner {
|
|
|
257
301
|
private shutdownHandler: ShutdownHandler = () => {};
|
|
258
302
|
private shortcutDiagnostics: ResourceDiagnostic[] = [];
|
|
259
303
|
private commandDiagnostics: ResourceDiagnostic[] = [];
|
|
304
|
+
/** Case-insensitive substring patterns matched against ext.path. ui.notify is dropped for matches. */
|
|
305
|
+
private quietExtensionPatterns: string[] = [];
|
|
260
306
|
|
|
261
307
|
constructor(
|
|
262
308
|
extensions: Extension[],
|
|
@@ -734,6 +780,41 @@ export class ExtensionRunner {
|
|
|
734
780
|
return eventType === "agent_end" || eventType === "stop" || eventType === "session_end";
|
|
735
781
|
}
|
|
736
782
|
|
|
783
|
+
/**
|
|
784
|
+
* Replace the quiet-extensions allowlist. Each entry is a case-insensitive
|
|
785
|
+
* substring matched against the extension's filesystem path. Matching
|
|
786
|
+
* extensions have their `ui.notify` calls silently dropped AND their
|
|
787
|
+
* `console.log/warn/error/info` calls silently dropped while the handler
|
|
788
|
+
* is on the stack — useful for muting noisy session_start banners
|
|
789
|
+
* (piolium uses ui.notify; pi-notion uses console.log).
|
|
790
|
+
*
|
|
791
|
+
* Console filtering uses AsyncLocalStorage so other extensions running
|
|
792
|
+
* concurrently are unaffected — output is only suppressed when the call
|
|
793
|
+
* originates inside a quiet extension's handler async context.
|
|
794
|
+
*/
|
|
795
|
+
setQuietExtensions(patterns: readonly string[]): void {
|
|
796
|
+
this.quietExtensionPatterns = patterns.map((p) => p.toLowerCase()).filter((p) => p.length > 0);
|
|
797
|
+
if (this.quietExtensionPatterns.length > 0) {
|
|
798
|
+
installQuietConsolePatch();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
private isQuietExtension(ext: Extension): boolean {
|
|
803
|
+
if (this.quietExtensionPatterns.length === 0) return false;
|
|
804
|
+
const path = ext.path.toLowerCase();
|
|
805
|
+
return this.quietExtensionPatterns.some((p) => path.includes(p));
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private withSilencedNotify(base: ExtensionContext): ExtensionContext {
|
|
809
|
+
// Wraps the shared ui context so notify() is a no-op while every other
|
|
810
|
+
// method (select/confirm/input/setStatus/setWidget/…) keeps working.
|
|
811
|
+
const silencedUi: ExtensionUIContext = {
|
|
812
|
+
...base.ui,
|
|
813
|
+
notify: () => undefined,
|
|
814
|
+
};
|
|
815
|
+
return { ...base, ui: silencedUi };
|
|
816
|
+
}
|
|
817
|
+
|
|
737
818
|
createCommandContext(): ExtensionCommandContext {
|
|
738
819
|
return {
|
|
739
820
|
...this.createContext(),
|
|
@@ -782,10 +863,18 @@ export class ExtensionRunner {
|
|
|
782
863
|
const handlers = ext.handlers.get(eventType);
|
|
783
864
|
if (!handlers || handlers.length === 0) continue;
|
|
784
865
|
|
|
866
|
+
const isQuiet = this.isQuietExtension(ext);
|
|
867
|
+
const extCtx = isQuiet ? this.withSilencedNotify(ctx) : ctx;
|
|
868
|
+
|
|
785
869
|
for (const handler of handlers) {
|
|
786
870
|
try {
|
|
787
871
|
const event = getEvent();
|
|
788
|
-
|
|
872
|
+
// Run quiet extensions inside an AsyncLocalStorage scope so
|
|
873
|
+
// the patched console methods can suppress their output
|
|
874
|
+
// without affecting non-quiet extensions running concurrently.
|
|
875
|
+
const handlerResult = isQuiet
|
|
876
|
+
? await quietExtensionContext.run(true, () => handler(event, extCtx))
|
|
877
|
+
: await handler(event, extCtx);
|
|
789
878
|
const action = processResult(handlerResult, ext.path);
|
|
790
879
|
if (action.done) return;
|
|
791
880
|
} catch (err) {
|
|
@@ -807,12 +807,13 @@ export class DefaultPackageManager implements PackageManager {
|
|
|
807
807
|
const parsed = this.parseSource(source);
|
|
808
808
|
const scope: SourceScope = options?.local ? "project" : "user";
|
|
809
809
|
await this.withProgress("install", source, `Installing ${source}...`, async () => {
|
|
810
|
+
// Explicit user-initiated install — stream npm/git progress to the terminal.
|
|
810
811
|
if (parsed.type === "npm") {
|
|
811
|
-
await this.installNpm(parsed, scope, false);
|
|
812
|
+
await this.installNpm(parsed, scope, false, true);
|
|
812
813
|
return;
|
|
813
814
|
}
|
|
814
815
|
if (parsed.type === "git") {
|
|
815
|
-
await this.installGit(parsed, scope);
|
|
816
|
+
await this.installGit(parsed, scope, true);
|
|
816
817
|
return;
|
|
817
818
|
}
|
|
818
819
|
if (parsed.type === "local") {
|
|
@@ -831,7 +832,7 @@ export class DefaultPackageManager implements PackageManager {
|
|
|
831
832
|
const scope: SourceScope = options?.local ? "project" : "user";
|
|
832
833
|
await this.withProgress("remove", source, `Removing ${source}...`, async () => {
|
|
833
834
|
if (parsed.type === "npm") {
|
|
834
|
-
await this.uninstallNpm(parsed, scope);
|
|
835
|
+
await this.uninstallNpm(parsed, scope, true);
|
|
835
836
|
return;
|
|
836
837
|
}
|
|
837
838
|
if (parsed.type === "git") {
|
|
@@ -869,15 +870,16 @@ export class DefaultPackageManager implements PackageManager {
|
|
|
869
870
|
const parsed = this.parseSource(source);
|
|
870
871
|
if (parsed.type === "npm") {
|
|
871
872
|
if (parsed.pinned) return;
|
|
873
|
+
// Explicit `pi package update` — stream npm progress to terminal.
|
|
872
874
|
await this.withProgress("update", source, `Updating ${source}...`, async () => {
|
|
873
|
-
await this.installNpm(parsed, scope, false);
|
|
875
|
+
await this.installNpm(parsed, scope, false, true);
|
|
874
876
|
});
|
|
875
877
|
return;
|
|
876
878
|
}
|
|
877
879
|
if (parsed.type === "git") {
|
|
878
880
|
if (parsed.pinned) return;
|
|
879
881
|
await this.withProgress("update", source, `Updating ${source}...`, async () => {
|
|
880
|
-
await this.updateGit(parsed, scope);
|
|
882
|
+
await this.updateGit(parsed, scope, true);
|
|
881
883
|
});
|
|
882
884
|
return;
|
|
883
885
|
}
|
|
@@ -890,54 +892,75 @@ export class DefaultPackageManager implements PackageManager {
|
|
|
890
892
|
): Promise<void> {
|
|
891
893
|
for (const { pkg, scope } of sources) {
|
|
892
894
|
const sourceStr = typeof pkg === "string" ? pkg : pkg.source;
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
895
|
+
try {
|
|
896
|
+
await this.resolveOnePackageSource(pkg, scope, accumulator, onMissing);
|
|
897
|
+
} catch (error) {
|
|
898
|
+
// One bad source (typo, 404, network glitch, unreachable git host)
|
|
899
|
+
// must not abort startup or block resolution of the remaining
|
|
900
|
+
// packages. Warn loudly so the user knows, then continue.
|
|
901
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
902
|
+
process.stderr.write(
|
|
903
|
+
`[pi] Skipping package "${sourceStr}" — ${message}. ` +
|
|
904
|
+
`Run \`pi remove ${sourceStr}\` if you want to drop it permanently.\n`,
|
|
905
|
+
);
|
|
901
906
|
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
902
909
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
910
|
+
private async resolveOnePackageSource(
|
|
911
|
+
pkg: PackageSource,
|
|
912
|
+
scope: SourceScope,
|
|
913
|
+
accumulator: ResourceAccumulator,
|
|
914
|
+
onMissing?: (source: string) => Promise<MissingSourceAction>,
|
|
915
|
+
): Promise<void> {
|
|
916
|
+
const sourceStr = typeof pkg === "string" ? pkg : pkg.source;
|
|
917
|
+
const filter = typeof pkg === "object" ? pkg : undefined;
|
|
918
|
+
const parsed = this.parseSource(sourceStr);
|
|
919
|
+
const metadata: PathMetadata = { source: sourceStr, scope, origin: "package" };
|
|
920
|
+
|
|
921
|
+
if (parsed.type === "local") {
|
|
922
|
+
const baseDir = this.getBaseDirForScope(scope);
|
|
923
|
+
this.resolveLocalExtensionSource(parsed, accumulator, filter, metadata, baseDir);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const installMissing = async (): Promise<boolean> => {
|
|
928
|
+
if (isOfflineModeEnabled()) {
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
if (!onMissing) {
|
|
914
932
|
await this.installParsedSource(parsed, scope);
|
|
915
933
|
return true;
|
|
916
|
-
}
|
|
934
|
+
}
|
|
935
|
+
const action = await onMissing(sourceStr);
|
|
936
|
+
if (action === "skip") return false;
|
|
937
|
+
if (action === "error") throw new Error(`Missing source: ${sourceStr}`);
|
|
938
|
+
await this.installParsedSource(parsed, scope);
|
|
939
|
+
return true;
|
|
940
|
+
};
|
|
917
941
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
}
|
|
925
|
-
metadata.baseDir = installedPath;
|
|
926
|
-
this.collectPackageResources(installedPath, accumulator, filter, metadata);
|
|
927
|
-
continue;
|
|
942
|
+
if (parsed.type === "npm") {
|
|
943
|
+
const installedPath = this.getNpmInstallPath(parsed, scope);
|
|
944
|
+
const needsInstall = !existsSync(installedPath) || (await this.npmNeedsUpdate(parsed, installedPath));
|
|
945
|
+
if (needsInstall) {
|
|
946
|
+
const installed = await installMissing();
|
|
947
|
+
if (!installed) return;
|
|
928
948
|
}
|
|
949
|
+
metadata.baseDir = installedPath;
|
|
950
|
+
this.collectPackageResources(installedPath, accumulator, filter, metadata);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
929
953
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
}
|
|
938
|
-
metadata.baseDir = installedPath;
|
|
939
|
-
this.collectPackageResources(installedPath, accumulator, filter, metadata);
|
|
954
|
+
if (parsed.type === "git") {
|
|
955
|
+
const installedPath = this.getGitInstallPath(parsed, scope);
|
|
956
|
+
if (!existsSync(installedPath)) {
|
|
957
|
+
const installed = await installMissing();
|
|
958
|
+
if (!installed) return;
|
|
959
|
+
} else if (scope === "temporary" && !parsed.pinned && !isOfflineModeEnabled()) {
|
|
960
|
+
await this.refreshTemporaryGitSource(parsed, sourceStr);
|
|
940
961
|
}
|
|
962
|
+
metadata.baseDir = installedPath;
|
|
963
|
+
this.collectPackageResources(installedPath, accumulator, filter, metadata);
|
|
941
964
|
}
|
|
942
965
|
}
|
|
943
966
|
|
|
@@ -1169,21 +1192,34 @@ export class DefaultPackageManager implements PackageManager {
|
|
|
1169
1192
|
return { name, version };
|
|
1170
1193
|
}
|
|
1171
1194
|
|
|
1172
|
-
private async installNpm(
|
|
1195
|
+
private async installNpm(
|
|
1196
|
+
source: NpmSource,
|
|
1197
|
+
scope: SourceScope,
|
|
1198
|
+
temporary: boolean,
|
|
1199
|
+
interactive: boolean = false,
|
|
1200
|
+
): Promise<void> {
|
|
1173
1201
|
const installRoot = this.getNpmInstallRoot(scope, temporary);
|
|
1174
1202
|
this.ensureNpmProject(installRoot);
|
|
1175
|
-
await this.runCommand("npm", ["install", source.spec, "--prefix", installRoot]);
|
|
1203
|
+
await this.runCommand("npm", ["install", source.spec, "--prefix", installRoot], { interactive });
|
|
1176
1204
|
}
|
|
1177
1205
|
|
|
1178
|
-
private async uninstallNpm(
|
|
1206
|
+
private async uninstallNpm(
|
|
1207
|
+
source: NpmSource,
|
|
1208
|
+
scope: SourceScope,
|
|
1209
|
+
interactive: boolean = false,
|
|
1210
|
+
): Promise<void> {
|
|
1179
1211
|
const installRoot = this.getNpmInstallRoot(scope, false);
|
|
1180
1212
|
if (!existsSync(installRoot)) {
|
|
1181
1213
|
return;
|
|
1182
1214
|
}
|
|
1183
|
-
await this.runCommand("npm", ["uninstall", source.name, "--prefix", installRoot]);
|
|
1215
|
+
await this.runCommand("npm", ["uninstall", source.name, "--prefix", installRoot], { interactive });
|
|
1184
1216
|
}
|
|
1185
1217
|
|
|
1186
|
-
private async installGit(
|
|
1218
|
+
private async installGit(
|
|
1219
|
+
source: GitSource,
|
|
1220
|
+
scope: SourceScope,
|
|
1221
|
+
interactive: boolean = false,
|
|
1222
|
+
): Promise<void> {
|
|
1187
1223
|
const targetDir = this.getGitInstallPath(source, scope);
|
|
1188
1224
|
if (existsSync(targetDir)) {
|
|
1189
1225
|
return;
|
|
@@ -1194,40 +1230,44 @@ export class DefaultPackageManager implements PackageManager {
|
|
|
1194
1230
|
}
|
|
1195
1231
|
mkdirSync(dirname(targetDir), { recursive: true });
|
|
1196
1232
|
|
|
1197
|
-
await this.runCommand("git", ["clone", source.repo, targetDir]);
|
|
1233
|
+
await this.runCommand("git", ["clone", source.repo, targetDir], { interactive });
|
|
1198
1234
|
if (source.ref) {
|
|
1199
|
-
await this.runCommand("git", ["checkout", source.ref], { cwd: targetDir });
|
|
1235
|
+
await this.runCommand("git", ["checkout", source.ref], { cwd: targetDir, interactive });
|
|
1200
1236
|
}
|
|
1201
1237
|
const packageJsonPath = join(targetDir, "package.json");
|
|
1202
1238
|
if (existsSync(packageJsonPath)) {
|
|
1203
|
-
await this.runCommand("npm", ["install"], { cwd: targetDir });
|
|
1239
|
+
await this.runCommand("npm", ["install"], { cwd: targetDir, interactive });
|
|
1204
1240
|
}
|
|
1205
1241
|
}
|
|
1206
1242
|
|
|
1207
|
-
private async updateGit(
|
|
1243
|
+
private async updateGit(
|
|
1244
|
+
source: GitSource,
|
|
1245
|
+
scope: SourceScope,
|
|
1246
|
+
interactive: boolean = false,
|
|
1247
|
+
): Promise<void> {
|
|
1208
1248
|
const targetDir = this.getGitInstallPath(source, scope);
|
|
1209
1249
|
if (!existsSync(targetDir)) {
|
|
1210
|
-
await this.installGit(source, scope);
|
|
1250
|
+
await this.installGit(source, scope, interactive);
|
|
1211
1251
|
return;
|
|
1212
1252
|
}
|
|
1213
1253
|
|
|
1214
1254
|
// Fetch latest from remote (handles force-push by getting new history)
|
|
1215
|
-
await this.runCommand("git", ["fetch", "--prune", "origin"], { cwd: targetDir });
|
|
1255
|
+
await this.runCommand("git", ["fetch", "--prune", "origin"], { cwd: targetDir, interactive });
|
|
1216
1256
|
|
|
1217
1257
|
// Reset to tracking branch. Fall back to origin/HEAD when no upstream is configured.
|
|
1218
1258
|
try {
|
|
1219
|
-
await this.runCommand("git", ["reset", "--hard", "@{upstream}"], { cwd: targetDir });
|
|
1259
|
+
await this.runCommand("git", ["reset", "--hard", "@{upstream}"], { cwd: targetDir, interactive });
|
|
1220
1260
|
} catch {
|
|
1221
|
-
await this.runCommand("git", ["remote", "set-head", "origin", "-a"], { cwd: targetDir }).catch(() => {});
|
|
1222
|
-
await this.runCommand("git", ["reset", "--hard", "origin/HEAD"], { cwd: targetDir });
|
|
1261
|
+
await this.runCommand("git", ["remote", "set-head", "origin", "-a"], { cwd: targetDir, interactive }).catch(() => {});
|
|
1262
|
+
await this.runCommand("git", ["reset", "--hard", "origin/HEAD"], { cwd: targetDir, interactive });
|
|
1223
1263
|
}
|
|
1224
1264
|
|
|
1225
1265
|
// Clean untracked files (extensions should be pristine)
|
|
1226
|
-
await this.runCommand("git", ["clean", "-fdx"], { cwd: targetDir });
|
|
1266
|
+
await this.runCommand("git", ["clean", "-fdx"], { cwd: targetDir, interactive });
|
|
1227
1267
|
|
|
1228
1268
|
const packageJsonPath = join(targetDir, "package.json");
|
|
1229
1269
|
if (existsSync(packageJsonPath)) {
|
|
1230
|
-
await this.runCommand("npm", ["install"], { cwd: targetDir });
|
|
1270
|
+
await this.runCommand("npm", ["install"], { cwd: targetDir, interactive });
|
|
1231
1271
|
}
|
|
1232
1272
|
}
|
|
1233
1273
|
|
|
@@ -1819,19 +1859,46 @@ export class DefaultPackageManager implements PackageManager {
|
|
|
1819
1859
|
};
|
|
1820
1860
|
}
|
|
1821
1861
|
|
|
1822
|
-
|
|
1862
|
+
/**
|
|
1863
|
+
* Run a child process. When `interactive` is true (the explicit user-facing
|
|
1864
|
+
* install/update path), stdio is inherited so npm/git progress shows in the
|
|
1865
|
+
* terminal. When false (the default — auto-resolve during launch), stdout
|
|
1866
|
+
* and stderr are captured. The captured stderr is appended to the failure
|
|
1867
|
+
* error so callers can surface the underlying cause without polluting the
|
|
1868
|
+
* TUI during startup.
|
|
1869
|
+
*/
|
|
1870
|
+
private runCommand(
|
|
1871
|
+
command: string,
|
|
1872
|
+
args: string[],
|
|
1873
|
+
options?: { cwd?: string; interactive?: boolean },
|
|
1874
|
+
): Promise<void> {
|
|
1875
|
+
const interactive = options?.interactive === true;
|
|
1823
1876
|
return new Promise((resolvePromise, reject) => {
|
|
1824
1877
|
const child = spawn(command, args, {
|
|
1825
1878
|
cwd: options?.cwd,
|
|
1826
|
-
stdio: "inherit",
|
|
1879
|
+
stdio: interactive ? "inherit" : ["ignore", "pipe", "pipe"],
|
|
1827
1880
|
shell: process.platform === "win32",
|
|
1828
1881
|
});
|
|
1882
|
+
|
|
1883
|
+
let stderr = "";
|
|
1884
|
+
if (!interactive) {
|
|
1885
|
+
child.stdout?.on("data", () => { /* drop — progress goes through withProgress */ });
|
|
1886
|
+
child.stderr?.on("data", (chunk: Buffer | string) => {
|
|
1887
|
+
stderr += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
1888
|
+
// Cap to avoid unbounded growth on chatty failures.
|
|
1889
|
+
if (stderr.length > 4_000) stderr = stderr.slice(-4_000);
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1829
1893
|
child.on("error", reject);
|
|
1830
1894
|
child.on("exit", (code) => {
|
|
1831
1895
|
if (code === 0) {
|
|
1832
1896
|
resolvePromise();
|
|
1833
1897
|
} else {
|
|
1834
|
-
|
|
1898
|
+
const detail = !interactive && stderr.trim().length > 0
|
|
1899
|
+
? `: ${stderr.trim().split("\n").slice(-3).join(" ").slice(0, 500)}`
|
|
1900
|
+
: "";
|
|
1901
|
+
reject(new Error(`${command} ${args.join(" ")} failed with code ${code}${detail}`));
|
|
1835
1902
|
}
|
|
1836
1903
|
});
|
|
1837
1904
|
});
|
|
@@ -24,6 +24,20 @@ export const SAFE_COMMAND_PREFIXES = [
|
|
|
24
24
|
"lpass",
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Whether to emit the per-blocked-command stderr warning. Off by default —
|
|
29
|
+
* extensions (e.g. piolium's anthropic-vertex registration) intentionally
|
|
30
|
+
* register !sh-style apiKey expressions that will never resolve on most user
|
|
31
|
+
* machines; the warning is noise. Opt back in by setting OTTO_LOG_BLOCKED_COMMANDS=1
|
|
32
|
+
* (or any truthy value) when actively debugging credential resolution.
|
|
33
|
+
*/
|
|
34
|
+
function shouldLogBlockedCommands(): boolean {
|
|
35
|
+
const v = process.env.OTTO_LOG_BLOCKED_COMMANDS;
|
|
36
|
+
if (!v) return false;
|
|
37
|
+
const lv = v.toLowerCase();
|
|
38
|
+
return lv === "1" || lv === "true" || lv === "yes";
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
/**
|
|
28
42
|
* Active command prefix allowlist. Defaults to SAFE_COMMAND_PREFIXES but can be
|
|
29
43
|
* overridden via setAllowedCommandPrefixes() (called from settings or env var).
|
|
@@ -70,13 +84,17 @@ function executeCommand(commandConfig: string): string | undefined {
|
|
|
70
84
|
const tokens = command.split(/\s+/).filter(Boolean);
|
|
71
85
|
const firstToken = tokens[0];
|
|
72
86
|
if (!activeCommandPrefixes.includes(firstToken)) {
|
|
73
|
-
|
|
87
|
+
if (shouldLogBlockedCommands()) {
|
|
88
|
+
process.stderr.write(`[resolve-config-value] Blocked disallowed command: "${firstToken}". Allowed: ${activeCommandPrefixes.join(", ")}\n`);
|
|
89
|
+
}
|
|
74
90
|
commandResultCache.set(commandConfig, undefined);
|
|
75
91
|
return undefined;
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
if (SHELL_OPERATORS.test(command)) {
|
|
79
|
-
|
|
95
|
+
if (shouldLogBlockedCommands()) {
|
|
96
|
+
process.stderr.write(`[resolve-config-value] Blocked shell operators in command: "${command}"\n`);
|
|
97
|
+
}
|
|
80
98
|
commandResultCache.set(commandConfig, undefined);
|
|
81
99
|
return undefined;
|
|
82
100
|
}
|
|
@@ -220,6 +220,11 @@ export interface Settings {
|
|
|
220
220
|
allowedCommandPrefixes?: string[]; // Override built-in SAFE_COMMAND_PREFIXES for !command resolution (global-only — ignored in project settings)
|
|
221
221
|
fetchAllowedUrls?: string[]; // Hostnames exempted from SSRF blocklist in fetch_page (global-only — ignored in project settings)
|
|
222
222
|
hooks?: HooksSettings; // Layer 0 shell-command hooks. Project-scoped hooks require explicit trust (.pi/hooks.trusted).
|
|
223
|
+
seedDefaultsOnLaunch?: boolean; // When true, ship-with-OTTO default packages are added to `packages` on launch
|
|
224
|
+
seededDefaults?: string[]; // Sources already attempted to seed — prevents re-adding after the user removes one
|
|
225
|
+
enabledDefaultPackages?: string[]; // Subset of default sources the user opted into during onboarding. undefined = "all" (back-compat)
|
|
226
|
+
quietExtensions?: string[]; // Substring patterns matched against extension.path — ui.notify calls from matching extensions are silently dropped
|
|
227
|
+
seededQuietPatterns?: string[]; // Quiet patterns already attempted to seed — zombie-resurrection guard, mirrors seededDefaults
|
|
223
228
|
}
|
|
224
229
|
|
|
225
230
|
/** Settings keys that are only respected from global config — project settings cannot override these. */
|
|
@@ -938,6 +943,57 @@ export class SettingsManager {
|
|
|
938
943
|
this.setProjectSetting("packages", packages);
|
|
939
944
|
}
|
|
940
945
|
|
|
946
|
+
getSeedDefaultsOnLaunch(): boolean | undefined {
|
|
947
|
+
return this.settings.seedDefaultsOnLaunch;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
setSeedDefaultsOnLaunch(enabled: boolean): void {
|
|
951
|
+
this.setGlobalSetting("seedDefaultsOnLaunch", enabled);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
getSeededDefaults(): string[] {
|
|
955
|
+
return [...(this.settings.seededDefaults ?? [])];
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
setSeededDefaults(sources: string[]): void {
|
|
959
|
+
this.setGlobalSetting("seededDefaults", sources);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Returns undefined when the user has expressed no preference (back-compat:
|
|
964
|
+
* the seeder treats this as "install every default"). Returns a (possibly
|
|
965
|
+
* empty) array when the user has explicitly chosen a subset.
|
|
966
|
+
*/
|
|
967
|
+
getEnabledDefaultPackages(): string[] | undefined {
|
|
968
|
+
const value = this.settings.enabledDefaultPackages;
|
|
969
|
+
return value === undefined ? undefined : [...value];
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
setEnabledDefaultPackages(sources: string[]): void {
|
|
973
|
+
this.setGlobalSetting("enabledDefaultPackages", sources);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Returns substring patterns matched (case-insensitive) against an extension's
|
|
978
|
+
* filesystem path. ui.notify calls from matching extensions are silently
|
|
979
|
+
* dropped at runtime — useful for muting noisy session_start banners.
|
|
980
|
+
*/
|
|
981
|
+
getQuietExtensions(): string[] {
|
|
982
|
+
return [...(this.settings.quietExtensions ?? [])];
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
setQuietExtensions(patterns: string[]): void {
|
|
986
|
+
this.setGlobalSetting("quietExtensions", patterns);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
getSeededQuietPatterns(): string[] {
|
|
990
|
+
return [...(this.settings.seededQuietPatterns ?? [])];
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
setSeededQuietPatterns(patterns: string[]): void {
|
|
994
|
+
this.setGlobalSetting("seededQuietPatterns", patterns);
|
|
995
|
+
}
|
|
996
|
+
|
|
941
997
|
getExtensionPaths(): string[] {
|
|
942
998
|
return [...(this.settings.extensions ?? [])];
|
|
943
999
|
}
|