PuterAppCommandProvider.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. /*
  2. * Copyright (C) 2024 Puter Technologies Inc.
  3. *
  4. * This file is part of Phoenix Shell.
  5. *
  6. * Phoenix Shell is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published
  8. * by the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. import { Exit } from '../coreutils/coreutil_lib/exit.js';
  20. import { signals } from '../../ansi-shell/signals.js';
  21. const BUILT_IN_APPS = [
  22. 'explorer',
  23. ];
  24. const lookup_app = async (id) => {
  25. if (BUILT_IN_APPS.includes(id)) {
  26. return { success: true, path: null };
  27. }
  28. const request = await fetch(`${puter.APIOrigin}/drivers/call`, {
  29. "headers": {
  30. "Content-Type": "application/json",
  31. "Authorization": `Bearer ${puter.authToken}`,
  32. },
  33. "body": JSON.stringify({ interface: 'puter-apps', method: 'read', args: { id: { name: id } } }),
  34. "method": "POST",
  35. });
  36. const { success, result } = await request.json();
  37. return { success, path: result?.index_url };
  38. };
  39. export class PuterAppCommandProvider {
  40. async lookup (id) {
  41. const { success, path } = await lookup_app(id);
  42. if (!success) return;
  43. return {
  44. name: id,
  45. path: path ?? 'Built-in Puter app',
  46. // TODO: Let apps expose option/positional definitions like builtins do, and parse them here?
  47. async execute(ctx) {
  48. const args = {
  49. command_line: {
  50. args: ctx.locals.args,
  51. },
  52. env: {...ctx.env},
  53. };
  54. const child = await puter.ui.launchApp(id, args);
  55. // Wait for app to close.
  56. const app_close_promise = new Promise((resolve, reject) => {
  57. child.on('close', () => {
  58. // TODO: Exit codes for apps
  59. resolve({ done: true });
  60. });
  61. });
  62. // Wait for SIGINT
  63. const sigint_promise = new Promise((resolve, reject) => {
  64. ctx.externs.sig.on((signal) => {
  65. if (signal === signals.SIGINT) {
  66. child.close();
  67. reject(new Exit(130));
  68. }
  69. });
  70. });
  71. // We don't connect stdio to non-SDK apps, because they won't make use of it.
  72. if (child.usesSDK) {
  73. const decoder = new TextDecoder();
  74. child.on('message', message => {
  75. if (message.$ === 'stdout') {
  76. ctx.externs.out.write(decoder.decode(message.data));
  77. }
  78. });
  79. // Repeatedly copy data from stdin to the child, while it's running.
  80. // DRY: Initially copied from PathCommandProvider
  81. let data, done;
  82. const next_data = async () => {
  83. ({ value: data, done } = await Promise.race([
  84. app_close_promise, sigint_promise, ctx.externs.in_.read(),
  85. ]));
  86. if (data) {
  87. child.postMessage({
  88. $: 'stdin',
  89. data: data,
  90. });
  91. if (!done) setTimeout(next_data, 0);
  92. }
  93. };
  94. setTimeout(next_data, 0);
  95. }
  96. return Promise.race([ app_close_promise, sigint_promise ]);
  97. }
  98. };
  99. }
  100. // Only a single Puter app can match a given name
  101. async lookupAll (...a) {
  102. const result = await this.lookup(...a);
  103. if ( result ) {
  104. return [ result ];
  105. }
  106. return undefined;
  107. }
  108. async complete (query, { ctx }) {
  109. if (query === '') return [];
  110. const results = [];
  111. for (const app_name of BUILT_IN_APPS) {
  112. if (app_name.startsWith(query)) {
  113. results.push(app_name);
  114. }
  115. }
  116. const request = await fetch(`${puter.APIOrigin}/drivers/call`, {
  117. "headers": {
  118. "Content-Type": "application/json",
  119. "Authorization": `Bearer ${puter.authToken}`,
  120. },
  121. "body": JSON.stringify({ interface: 'puter-apps', method: 'select', args: { predicate: [ 'name-like', query + '%' ] } }),
  122. "method": "POST",
  123. });
  124. const json = await request.json();
  125. if (json.success) {
  126. for (const app of json.result) {
  127. results.push(app.name);
  128. }
  129. }
  130. return results;
  131. }
  132. }